Generic functions
A generic function is a function over types. The checker reads the call arguments and solves for each type parameter — but a parameter used only once is just `any` wearing a costume.
A teammate writes a first<T>(arr: T[]): T helper and is delighted: “It’s generic, it works on any array!” Two weeks later someone writes identity<T>(x: unknown): T and casts the world into it. Both have a <T>. Only one of them is actually doing anything — the other is any wearing a costume, and the compiler will never warn you. By the end of this lesson you’ll know exactly which one breaks and how to spot it in code review.
A function over types
A normal function abstracts over values: you pass 3 or "x" and the body runs the same way. A generic function abstracts over types: you pass a type (explicitly or, far more often, implicitly) and the signature specialises to it.
function identity<T>(x: T): T {
return x;
}
const a = identity("hello");
// ^? const a: "hello"
const b = identity(42);
// ^? const b: numberThe <T> declares a type parameter — a placeholder in type space, exactly like a value parameter is a placeholder in value space. At each call site the checker picks a concrete type for T and the return type follows.
Inference: the checker solves for T
You almost never write identity<string>("hello"). Instead the checker performs type argument inference: it matches each argument’s type against the parameter that uses the type variable and solves for it.
function wrap<T>(value: T): { value: T } {
return { value };
}
const w = wrap([1, 2, 3]);
// ^? const w: { value: number[] }Here value: T is matched against the argument [1, 2, 3] (inferred as number[]), so T = number[], and the return type is computed as { value: number[] }. The inference flows from the argument position into T, then back out through the return position.
Explicit vs inferred type arguments
You can supply the type argument by hand, and sometimes you must — when there are no arguments to infer from, or when you want a wider type than inference would pick.
function make<T>(): T[] {
return [];
}
const xs = make();
// ^? const xs: unknown[] — nothing to infer from, T falls to unknown
const ys = make<string>();
// ^? const ys: string[] — explicit type argumentWhen you write nothing and there is nothing to infer, an unconstrained T resolves to unknown (not any) under modern defaults. Explicit type arguments are also useful to narrow inference: wrap<readonly number[]>([1, 2, 3]) forces a readonly element type that inference alone would not pick.
Multiple type parameters, and pluck
Real signatures often carry several type variables that the checker solves independently:
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const p = pair("id", 7);
// ^? const p: [string, number]The single most useful generic shape in application code is pluck / prop — read a key off an object and get back exactly that property’s type. (The K extends keyof T machinery is the next lesson; here, just see why two parameters are needed.)
function prop<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Ada" };
const n = prop(user, "name");
// ^? const n: string
const id = prop(user, "id");
// ^? const id: numberT is solved from obj, K from key, and the return type T[K] (an indexed access — that was last lesson) gives back the precise property type. Two parameters, each appearing twice.
The “appears at least twice” rule
Here is the rule that separates real generics from cargo-cult ones: a type parameter that appears only once in the signature is almost always a mistake. A type variable earns its keep by relating two positions — an argument to a return, or two arguments to each other. If it appears once, it relates nothing, and it is silently equivalent to any at that spot.
// SMELL: T appears only in the parameter, never related to anything.
function logAndDrop<T>(x: T): void {
console.log(x);
}
// Identical in behaviour and safety to:
function logAndDrop2(x: unknown): void {
console.log(x);
}
// WORSE: T appears only in the return — the caller invents T from nothing.
function parse<T>(text: string): T {
return JSON.parse(text);
}
const cfg = parse<{ port: number }>("{}");
// ^? const cfg: { port: number } — a LIE; runtime value is {}In parse<T>, T is unconstrained by any argument, so the caller’s annotation is an unchecked assertion. That single-use return-position T is one of the most common sources of “the types said it was fine, production said otherwise.”
What over-generic code costs the compiler
A <T> that earns its keep is nearly free; a <T> sprinkled everywhere is not. The number to watch is type instantiations, which tsc --extendedDiagnostics reports. A leaf utility module that types its data plainly might show on the order of tens of thousands of instantiations; rewrite the same helpers so every signature is generic and chained (each call re-solving T and substituting it through several more generic callees) and the count climbs into the hundreds of thousands — Instantiations: 412,300 against a former 38,900 is a realistic delta for one over-abstracted utilities file, and it shows up directly as a few hundred extra milliseconds of Check time on each incremental rebuild. Generics are a compile-time loop the checker runs per call site; cheap per instantiation, but you can write a lot of them.
The runaway case is recursive generics. The checker caps how deeply it will instantiate a generic alias before giving up: hit roughly 50 levels of nesting and you get Type instantiation is excessively deep and possibly infinite (TS error 2589) — the same depth ceiling that bounds recursive conditional types. A single fluent builder typed as Builder<readonly [A]> → Builder<readonly [A, B]> → … walks straight into this wall once a chain gets long, and the only fixes are to break the recursion or fall back to a non-generic shape.
▸Why this works
Why does the once-rule hold? A type parameter is a relationship between positions. identity<T>(x: T): T is meaningful because it promises “the thing out has the same type as the thing in.” Delete one of those two Ts and the promise disappears — there is no longer a relationship to enforce, so the variable carries no information. Before adding a <T>, count its occurrences: fewer than two, and you have written any (or, for the return-only case, an assertion in disguise).
The tradeoff a senior weighs is flexibility against two costs: readability at the call site and instantiation work for the checker. A generic that relates two positions buys real safety and is worth both costs. A generic with a once-used parameter buys nothing and pays both — a harder signature to read and an extra instantiation per call. The discipline is not “prefer generics”; it is “introduce a type parameter only when it ties positions together, and stop generalizing the moment a concrete type would read more plainly and check just as soundly.”
Given `function wrap<T>(value: T): { value: T }`, what is the inferred type of `wrap(new Date())`?
Why is `function parse<T>(text: string): T { return JSON.parse(text) }` considered unsafe?
Order the steps the checker takes to type the call `prop(user, 'name')` for `prop<T, K extends keyof T>(obj: T, key: K): T[K]`:
- 1 Match argument `user` against parameter `obj: T` and solve T = { id: number; name: string }
- 2 Match argument `'name'` against parameter `key: K`, constrained by `K extends keyof T`
- 3 Solve K to the literal type `'name'`
- 4 Substitute T and K into the return type T[K]
- 5 Report the call's result type as `string`
Fill in the blank: a type parameter earns its place by appearing at least _______ times in the signature, because its job is to relate one position to another.
- 01Explain, with the `wrap<T>(value: T): { value: T }` example, what 'type argument inference' means and which direction information flows.
- 02State the 'appears at least twice' rule and give one parameter-only and one return-only example of how it is violated.
A generic function abstracts over types the way an ordinary function abstracts over values. The checker performs type argument inference — matching each call argument against the parameter that mentions a type variable, solving for it, and substituting it back through the return type. Explicit type arguments exist for the cases where there is nothing to infer or you need a wider type. The decisive smell test is the “appears at least twice” rule: a once-used parameter is any or an assertion in costume. Next we add extends constraints, which both restrict who may call a generic and unlock the members you can touch inside it. Now when you see a <T> in a signature, your first question is: how many times does it appear — and what two positions does it actually relate?
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.