The problem generics solve
T is a parameter for a type: the caller supplies it, usually without noticing, and the compiler carries it through. Without it you are choosing between a function that works on one type and a function that checks nothing.
function first(list: any[]): any { return list[0]; }
const door = first(doors); // any — everything after this is unchecked
function first<T>(list: T[]): T | undefined { return list[0]; }
const door = first(doors); // Door | undefined
Inference does the work
const names = first(["a", "b"]); // T is string
const nums = first([1, 2]); // T is number
const doors = first<Door>([]); // and you can say so when inference cannot
Constraints
function byId<T extends { id: string }>(list: T[], id: string): T | undefined {
return list.find((item) => item.id === id);
}
byId(doors, "a"); // Door | undefined — the full type, not { id: string }
byId([1, 2], "a"); // Error: number does not have an id
The function above only reads id, so that is all it asks for. It then works for doors, orders, users — anything with an id.
keyof, and a typed property getter
keyof Door is the union "id" | "name" | "days" | "price", and T[K] is the type of that property. This is where generics stop being a syntax and start being useful.
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
pluck(doors, "name"); // string[]
pluck(doors, "price"); // number[]
pluck(doors, "nope"); // Error: "nope" is not a key of Door
Defaults
interface Result<T = unknown> {
status: number;
data: T;
}
const a: Result = …; // data: unknown
const b: Result<Door[]> = …; // data: Door[]
Generic types, not only functions
type Box<T> = { value: T; at: Date };
class Store<T extends { id: string }> {
#items: T[] = [];
add(item: T): this { this.#items.push(item); return this; }
byId(id: string): T | undefined { return this.#items.find((i) => i.id === id); }
}
const doors = new Store<Door>();
Where it goes wrong
// pointless: T appears once, so nothing is connected
function log<T>(value: T): void { console.log(value); }
function log(value: unknown): void { … } // say what you mean
// and the classic mistake
function wrap<T>(value: T): T[] { return [value]; } // good: in and out
function wrap<T>(value: any): T[] { … } // bad: T is a lie the caller tells
It earns its place when it ties two things together: an argument to a return, or one argument to another. If it appears only once in the signature, replace it with unknown.
Reading a generic signature
Long generic errors read the same way: find the type parameters first, then what each was inferred as, and the mismatch is usually in the second line from the bottom.
function groupBy<T, K extends keyof T>(items: T[], key: K): Map<T[K], T[]>;
// ↑ the item ↑ one of its keys ↑ keyed by that property's type