How it works
The explanation, with examples you can run.
Linking input and output
function first<T>(xs: T[]): T | undefined { return xs[0]} const a = first([1, 2, 3]) // number | undefinedconst b = first(["x"]) // string | undefinedconst c = first<boolean>([]) // written out when there is nothing to infer fromCompare function first(xs: unknown[]): unknown. It is just as correct, but every caller has to check or cast the result again, because the function threw away what the caller knew.
Constraints
Inside a generic function, T could be anything, so you can't use any of its members. A constraint narrows what callers may pass and, in exchange, tells the body what it can rely on:
function byId<T extends { id: string }>(items: T[], id: string): T | undefined { return items.find((item) => item.id === id)} type Product = { id: string; title: string; price: number }declare const products: Product[] const p = byId(products, "p1") // Product | undefined, with title and pricebyId([1, 2, 3], "1") // Error: number does not have an idThe common partner of constraints is keyof, which ties a key to the object it belongs to:
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]} const user = { name: "Ada", age: 36 }const age = pluck(user, "age") // numberpluck(user, "email") // Error: "email" is not a key of the userT[K] is an indexed access type: the type of property K of T. Here two type parameters genuinely relate: K depends on T, and the result depends on both.
Generic types
Types take parameters too, which is how Array<T>, Promise<T> and Map<K, V> work:
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E } function parsePort(raw: string): Result<number, string> { const n = Number(raw) return Number.isInteger(n) ? { ok: true, value: n } : { ok: false, error: `not a port: ${raw}` }}E = Error is a default, used when the caller leaves the second argument out.
Restraint
The handbook gives three rules worth keeping: use the type parameter itself rather than constraining it more than needed, use as few type parameters as possible, and if a type parameter appears in only one place, you probably don't need it. function log<T>(value: T): void relates nothing: value: unknown says the same. Generics are for connecting types; when nothing needs connecting, a plain type gives simpler signatures and clearer error messages.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.