Generics
Date: 2026-09-28
A generic is a function at the type level: a type parameter goes in, a type comes out, and the compiler usually infers the argument from how you call it. The skill is placing the parameter where inference can see it — and stopping before the signature becomes harder to read than the code.
Generics are type parameters — placeholders in a function, type or class that are filled in per use, so one definition preserves the specific type flowing through it instead of widening to any or a shared supertype.
The problem they solve
function firstAny(xs: any[]): any { return xs[0] }
function firstUnknown(xs: unknown[]): unknown { return xs[0] }
function first<T>(xs: T[]): T | undefined { return xs[0] }
firstAny(orders) // any → checking switched off downstream
firstUnknown(orders) // unknown → safe, but you must narrow before every use
first(orders) // Order | undefined ← the information survived the callThe value of a generic is a relationship: the output type is tied to the input type. A type parameter used only once in a signature expresses no relationship and should usually be a plain type.
Three parts: parameter, constraint, inference site
function sumBy<T, K extends keyof T>(rows: T[], key: K): number
// │ └──────┬───────┘ └──┬──┘ └┬┘
// │ constraint inference inference
// │ K must be a site for T site for K
// │ key of T
// declares the parameters- Declaration —
<T>introduces a name, like a function parameter - Constraint —
extendsbounds what the argument may be.K extends keyof Tmeans “a key of whatever T turned out to be” — constraints can refer to other parameters - Inference site — a position in the value parameters where the compiler reads the argument’s type. No inference site, no inference: a parameter appearing only in the return type has to be supplied explicitly, which is usually a sign it’s a disguised type assertion
sumBy(orders, 'total') infers T = Order, K = 'total'. sumBy(orders, 'totl') fails at the call site, which is the point.
Note the constraint on K doesn’t restrict K to numeric fields — sumBy(orders, 'id') compiles. Tightening it needs a mapped-type constraint, and that’s the moment to ask whether the extra precision is worth the signature.
Controlling inference
Inference picks the widest reasonable candidate and blends every site it finds. Two tools, both verified:
const type parameters (TypeScript 5.0) ask for the literal type instead of the widened one:
function routes<const T extends readonly string[]>(r: T) { return r }
routes(['/', '/cart']) // T = readonly ['/', '/cart'] not string[]NoInfer<T> (5.4) marks a site as “check against T, but don’t use this to decide T”:
function pick<T extends string>(options: T[], fallback: NoInfer<T>): T
pick(['gbp', 'eur'], 'usd')
// ~~~~~ ✗ without NoInfer, T silently widens to include 'usd'Generic types
The same idea at the type level — this is how every utility type is built (Utility Types):
type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; error: string }
type Page<T> = { items: T[]; nextCursor: string | null }A default (<T = unknown>) is used when nothing is supplied or inferable. Defaults are a common place for any to hide — Promise<any> from a library default is a boundary in disguise.
Where they stop helping
- The lying generic.
function get<T>(url: string): Promise<T>looks typed and is an assertion — T is whatever the caller says, and nothing checks the response. It reads like validation and does none - Parameters nobody reads.
<T, U, V>with each used once is noise. Count relationships, not parameters - Error messages degrade fast. A deeply constrained signature produces errors about the constraint, not the call. If a colleague can’t read the error, the precision cost more than it bought
- Library code and app code differ. Heavy generics earn their place in a shared data-fetching or form library used hundreds of times. In application code, a concrete type is nearly always clearer
vs C#: C# generics exist at runtime (reified — typeof(T) works). TypeScript’s are erased entirely; there is no T to inspect when the code runs, which is why a generic can never validate anything.