You've reached the chapter most TypeScript learners quietly worry about. Take a breath — it's simpler than it looks. Generics are how you write a function (or interface, or class) once and have it work safely for many types. They're how Cypress's cy.get<HTMLInputElement>(...) keeps the right element type, how Array<T> keeps strings as strings and numbers as numbers, and how the typed retry helper from the last chapter remembered what it returned. This lesson starts at the very bottom and builds up slowly.
The problem generics solve
You write a helper that returns the first item of an array of users:
function getFirstUser(items: User[]): User {
return items[0];
}Then you need the same thing for products. And orders. And test cases. You don't want to copy-paste four nearly-identical functions. The first instinct is to use any:
function getFirst(items: any[]): any {
return items[0];
}
const first = getFirst(users);
// first is `any` — no autocomplete, no type checking 😢
first.name; // compiles
first.banana; // also compiles, also wrongany makes the function reusable but throws away every type guarantee. The caller has lost the link between "I passed a User[]" and "I should get a User back." This is exactly the problem TypeScript was supposed to fix.
The fix: a type parameter
A generic function takes a type as an argument, the same way a regular function takes a value as an argument. You write <T> after the name to declare a type parameter, then use T wherever you'd otherwise write a concrete type:
function getFirst<T>(items: T[]): T {
return items[0];
}
const user = getFirst(users); // T inferred as User → return type User
const product = getFirst(products); // T inferred as Product → return type Product
const code = getFirst([200, 201]); // T inferred as number → return type number
user.name; // ✅ autocomplete — TypeScript knows it's a User
user.banana; // ❌ Property 'banana' does not exist on type 'User'.The function definition stays the same; only the type parameter changes per call. Type safety preserved, code reused. That's the whole pitch.
A useful analogy: the mould
Think of a generic function like a mould for shaping things.
- The mould (the function logic — "return the first item") is the same regardless of what you pour in.
- The material (the actual type —
User,Product,number) changes per use. - TypeScript holds the mould steady and tracks which material you used, so it knows the shape of the thing that comes out.
A non-generic function is a single-purpose mould that only takes plaster. any is a sloppy mould that produces shapeless blobs. A generic function is a parametric mould — strong enough to hold shape, flexible enough to take anything.
How TypeScript figures out T
You almost never need to specify T explicitly. TypeScript infers it from the arguments at the call site:
getFirst(users); // inferred: T = User
getFirst([200, 201, 404]); // inferred: T = number
getFirst(["a", "b"]); // inferred: T = stringWhen you want to be explicit — for documentation, or because inference can't figure it out — pass the type in angle brackets:
getFirst<Product>(products); // explicit: T = ProductInferred is the default; explicit is the override. Both produce identical code at runtime — generics are erased after compilation, just like every other type.
T is just a name — pick what reads best
T is convention (it stands for "Type") and the right default. But it's just a name — you can use anything that's readable in context:
function findById<TItem>(items: TItem[], id: string): TItem | undefined { /* ... */ }
function map<TInput, TOutput>(items: TInput[], fn: (x: TInput) => TOutput): TOutput[] { /* ... */ }For helpers with a single type parameter, plain T is fine. For multiple parameters, descriptive names (TKey, TValue, TInput, TOutput) earn their length. Long names are noise on a one-letter helper but documentation on a four-parameter signature.
Generics are type arguments
The cleanest mental model: generics are like function arguments, but for types instead of values.
- Regular function:
getFirst(items)—itemsis a value argument. - Generic function:
getFirst<User>(items)—<User>is a type argument.
Functions take values to compute over. Generic functions take types to relate input shapes to output shapes. Once you see the parallel, the angle brackets stop looking exotic.
Side by side — the difference
Same function, three implementations — what each preserves
any — no safety
function getFirst(items: any[]): any
Compiles for every input shape
Return type is any — no autocomplete
user.banana compiles silently — bug ships
Reusable but unsafe — defeats TypeScript
Concrete — safe but rigid
function getFirstUser(items: User[]): User
Type-safe for User arrays
Doesn't work for Product, Order, anything else
You'd write four near-identical helpers
Safe but full of duplication
Generic — safe AND reusable
function getFirst<T>(items: T[]): T
T inferred per call from the argument
Return type matches the input element type
Autocomplete works — wrong properties rejected
One helper, every type, full safety
A first QA-shaped example
A typed test data builder that creates and validates a fixture object:
function makeFixture<T>(template: T, overrides: Partial<T> = {}): T {
return { ...template, ...overrides };
}
interface User { id: number; name: string; role: "admin" | "tester" | "viewer" }
const defaultUser: User = { id: 0, name: "default", role: "tester" };
const alice = makeFixture(defaultUser, { name: "Alice", role: "admin" });
// ^ alice is User — overrides type-checked against User's fields
const broken = makeFixture(defaultUser, { name: "Alice", banana: 42 });
// ❌ Object literal may only specify known properties, and 'banana' does not exist in type 'Partial<User>'.Partial<T> is one of TypeScript's built-in utility types — it makes every field of T optional. You'll meet it formally in this chapter's last lesson; for now, notice how the generic T and the Partial<T> together produce a fully type-safe factory that works for any fixture shape.
⚠️ Common mistakes
- Using
anyto avoid generics.function getFirst(items: any[]): anyis the giveaway — the function works on any array, but the caller loses every type guarantee. Almost any place you reach foranybecause "the function is reusable" is a place generics fit better. - Specifying
Texplicitly when inference would do.getFirst<User>(users)is correct but redundant — TypeScript readsUser[]and infersT = User. Explicit type arguments earn their place when inference can't figure it out (rare) or when you want documentation; otherwise, let inference work. - Treating
<T>as runtime metadata. Generics are erased at compile time. There is noTat runtime — you cannot doif (T === User) { ... }ornew T(). If you need runtime knowledge of the type, pass a value-level argument (a class reference, a tag string) explicitly.
🎯 Practice task
Write your first generic. 20-30 minutes.
- In your
ts-for-qa/srcfolder, creategenerics.ts. - Write a non-generic version first:
function getFirstUser(items: User[]): User. Hard-code an array of three users and confirm autocomplete on the result. - Now make it generic:
function getFirst<T>(items: T[]): T. Call it with the same users array, an array of numbers, and an array of strings. Confirm the return type changes per call (hover over each result in VS Code). - Trigger every key check. After each, read the error and revert:
- Pass an empty array — what's the runtime risk? (Hint: the type says
T, but the runtime returnsundefined.) - Try
getFirst(123)(not an array). - Set
const x: number = getFirst(users).
- Pass an empty array — what's the runtime risk? (Hint: the type says
- Write
function logAndReturn<T>(label: string, value: T): Tthat prints the label and value and returnsvalue. Use it to wrap two API calls of different return shapes — confirm each caller still gets the precise type. - Stretch: rewrite the
makeFixture<T>example. Pass it three different default objects (User, Product, Order) and confirm each call site has full autocomplete on the overrides.
The next lesson scales up: multiple type parameters, generic constraints, and the keyof operator that lets you write type-safe getProperty helpers.