Appearance
3.7.4 — Generics In Depth
Write a function that returns its argument. What's its type? (x: any) => any throws information away — pass a string, get back "could be anything." (x: string) => string works once, then you copy-paste it for numbers. The missing idea, from 3.3: a type variable. Generics let a function, interface, or class say "I work uniformly for some type T — and I promise consistent relationships between my inputs and outputs in terms of T." They are how libraries stay both reusable and precise, and they're where TypeScript rewards understanding over pattern-copying: constraints, keyof, overloads, and variance all follow from one mental model. This page builds it. ⚑Generics T and K,V; bounded type parameters. [EQ-204]
1. Type parameters: capturing the relationship
typescript
function identity<T>(x: T): T { // <T> declares a TYPE PARAMETER
return x;
}
const s = identity("hello"); // T inferred as "hello" → s: string (literal-widened)
const n = identity(42); // T inferred as 42 → n: numberRead <T> as: this function is parameterized by a type, chosen fresh at each call. The power isn't in accepting anything — any does that — it's in the relationship: the return type is the argument type. Information flows through instead of being laundered into any:
typescript
function firstAny(xs: any[]): any { return xs[0]; }
function first<T>(xs: T[]): T | undefined { return xs[0]; }
firstAny([1, 2]).toUpperCase(); // compiles — crashes at runtime
first([1, 2])?.toUpperCase(); // ❌ compile error: number has no toUpperCase ✅You almost never supply the parameter — inference reads it from the arguments (first([1,2]) binds T = number). Explicit first<number>([]) is for the cases inference can't see (empty containers, desired widening). Multiple parameters express multi-way relationships — the classic <K, V> pair on maps, or:
typescript
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] { /* … */ }— which we can't read yet. Two ingredients first.
2. Constraints: extends as "must at least be"
An unconstrained T could be anything, so the body may do almost nothing with it. A constraint bounds it:
typescript
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b; // .length is now legal — T promised it
}
longest("abc", "de"); // ✅ strings have length; returns string
longest([1, 2], [3]); // ✅ arrays too — returns number[]
longest(10, 20); // ❌ number doesn't satisfy { length: number }T extends X means T must be assignable to X — "bounded" generics ("at least an X, but I remember exactly which"). This beats typing the parameter as plain { length: number } for the usual reason: the constraint admits subtypes while T preserves the caller's precise type — longest("abc","de") returns string, not the anonymous {length} shape. Defaults complete the syntax: interface Box<T = string> { value: T } lets Box mean Box<string> unadorned — the tool for making an API pleasant to use all over React's typings.
3. keyof and indexed access: generics over structure
Two operators lift property names and property types into the type system:
typescript
type User = { id: string; age: number; admin: boolean };
type UserKey = keyof User; // "id" | "age" | "admin" — a literal union of keys
type AgeType = User["age"]; // number — indexed access
type ValTypes = User[keyof User]; // string | number | boolean — all value typeskeyof turns a type's keys into a literal union (3.7.2); indexed access T[K] reads a property's type. Combine them with a constrained parameter and you can type the most common "dynamic" idiom in JavaScript — safely:
typescript
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: "u1", age: 36, admin: false };
const age = getProp(user, "age"); // K inferred as "age" → return type number ✅
getProp(user, "email"); // ❌ '"email"' is not assignable to keyof TRead it as a sentence: for any object type T and any actual key K of it, given the object and that key, you get back exactly the type stored there. Keys are checked, return types are exact, refactors that rename a property break every misuse. This trio — T, K extends keyof T, T[K] — is the backbone of typed ORMs, form libraries, pluck/groupBy utilities, and the mapped types of 3.7.5. (It's also why Object.keys returning string[] — not keyof T — surprises people: structural typing means an object may have more keys than its type declares, so the honest key type at runtime is string.)
Generic interfaces and classes scale the same idea to whole structures — interface Result<T, E> { … }, class Queue<T> { push(x: T): void; pop(): T | undefined } — one definition, precise for every instantiation, exactly how Array<T>, Map<K, V>, and Promise<T> are declared.
4. Function overloads: many signatures, one body
Some functions' return type depends on which argument shape you called them with — beyond what one generic signature can say:
typescript
function parse(input: string): AST; // overload signature 1
function parse(input: string, loose: true): AST | null; // overload signature 2
function parse(input: string, loose?: boolean): AST | null { // IMPLEMENTATION
/* one body serving both — checked loosely against each overload */
}
parse("x = 1"); // typed AST — callers see only the overload list
parse("x = 1", true); // typed AST | nullCallers resolve against the overload list, top to bottom, first match wins (order matters — most specific first); the implementation signature is invisible to them. Honest guidance: overloads are the right tool when return types genuinely differ by call shape (DOM's createElement("div") → HTMLDivElement), but they're verbose and the implementation body is only loosely checked — so before writing overloads, try a union parameter, a generic with a constraint, or (for lookup-style APIs like createElement) a generic over a key: function create<K extends keyof TagMap>(tag: K): TagMap[K] replaces dozens of overloads with one line. Overloads are the fallback, not the default.
5. Variance: the direction subtyping flows
The deepest question generics raise: if Dog is a subtype of Animal, is Container<Dog> a subtype of Container<Animal>? The answer — it depends on how the container uses its parameter — is variance, and it explains several checker behaviors that look arbitrary.
Think in terms of two roles. A position that produces T (return values, readonly fields) is safe to narrow the source: something that hands out Dogs is a fine substitute where Animal-producers are expected — covariant, same direction as the subtype arrow. A position that consumes T (parameters, write slots) flips: a function eating any Animal can stand in where a Dog-eater is expected — but not the reverse (it might be fed a Cat) — contravariant, arrow reversed:
typescript
type Feeder = (a: Dog) => void;
const feedAnyAnimal: (a: Animal) => void = (a) => a.feed();
const f: Feeder = feedAnyAnimal; // ✅ consumer of the SUPERTYPE substitutes
const feedDogsOnly: (d: Dog) => void = (d) => d.fetch();
const g: (a: Animal) => void = feedDogsOnly; // ❌ might receive a CatNow the checker's "arbitrary" rules decode themselves. Mutable arrays are both producer and consumer, so sound typing would make Dog[] unrelated to Animal[]; TypeScript pragmatically treats arrays as covariant anyway (convenience over strictness — push an Animal into a Dog[] via an alias and you've built the one hole; readonly T[], being produce-only, is soundly covariant — one more reason it's the right parameter type). strictFunctionTypes (3.7.1) enforces contravariant parameters for function-typed variables — but method signatures stay bivariant (either direction accepted), a deliberate leniency without which Array<Dog> wouldn't be assignable to Array<Animal> at all. And the explicit annotations in/out (interface Producer<out T>, interface Consumer<in T>) let library authors declare variance — checked against usage — the same out/in you'll meet in C# and Kotlin.
You don't need variance daily. You need it the day a perfectly-reasonable-looking assignment errors with "types of parameters are incompatible" — and then the producer/consumer question answers it in one step.
6. The expert lens
A generic is a theorem, and inference is the proof-reader. <T>(xs: T[]) => T | undefined states a law: whatever element type you bring, that's what you get back. This view generates the two central style rules. Don't write return-only generics — function load<T>(url: string): Promise<T> has no argument to infer T from, so every call site is load<User>(…): an unchecked cast wearing generic syntax (make it earn the type via a schema argument instead: load(url, UserSchema)). And don't over-genericize — a T that appears exactly once in a signature adds no relationship; plain unknown or a concrete type is more honest. A useful generic's parameter appears at least twice: that's the relationship it exists to state.
Constraints are interface design. T extends { length: number } is structural typing's version of "program to an interface" (3.5): demand the minimum capability, remember the maximum information. Libraries live by this — demand narrow (keyof T, { id: string }), return precise (T, T[K]) — and it's why well-typed library calls feel like the library "knows" your data: your types flowed through the constraint, they were never replaced by it.
Variance knowledge transfers whole. Producer-covariant / consumer-contravariant is the same law in Java's ? extends/? super (PECS), C#'s and Kotlin's out/in, and the array-covariance holes of Java and TypeScript alike. Learn it once here, reuse it everywhere a generic container crosses a subtype boundary — including 3.7.5, where conditional types check assignability along exactly these rules.
Next: 3.7.5 — the level up: types computed from types — mapped, conditional, infer, template literals — and rebuilding the standard utility types from scratch.
Recall
<T>declares a type parameter; its value is the relationship it states between positions ((x: T) => T), preserved through calls — versusany, which launders. Inference bindsTfrom arguments; supply explicitly only when inference can't see (empty containers). Defaults:<T = string>.- Constraints
T extends X= "at least an X, but remember exactly which" — enables member access inside, admits subtypes outside, preserves caller precision. keyof T (keys as literal union) + indexed accessT[K]+K extends keyof T= the typed-property-access backbone (getProp, ORMs, form libs). - Overloads: ordered signature list, first match wins, implementation invisible & loosely checked — use when return type depends on call shape; prefer unions/constrained generics/key-lookup generics (
K extends keyof TagMap → TagMap[K]) first. - Variance: producers of
Tare covariant (subtype flows along), consumers are contravariant (flows backward —strictFunctionTypesenforces for function types; methods stay bivalent); mutable arrays are unsoundly covariant for convenience (readonly T[]is soundly so);in/outdeclare it. - Style laws: a good
Tappears ≥ twice; return-only generics are casts in disguise (earn types via schema arguments); demand the minimum (constraint), preserve the maximum (T).
Self-test: Why does first<T> beat firstAny — state it as information flow. What does T extends { length: number } buy over typing the parameter as { length: number }? Read getProp's signature aloud as a sentence. Why can a (a: Animal) => void substitute for a (d: Dog) => void but not vice versa? Why is load<T>(url): Promise<T> a design smell?
Quiz Bank
FoundationalWhat do generics give you that any does not?
Preserved relationships. any accepts everything by discarding type information — firstAny(xs).toUpperCase() compiles for a number array and crashes at runtime. A generic accepts everything by parameterizing: first<T>(xs: T[]): T | undefined states that the return type is the element type, so first([1,2]) is number | undefined and misuse fails at compile time. Inference binds T per call from the arguments, so callers pay no syntax. The mental model: a generic signature is a small theorem about input/output relationships, checked at every instantiation — which is also why a T appearing only once in a signature is a smell (no relationship stated; unknown is more honest).
FoundationalWhat does a generic constraint do? Compare T extends Lengthy against typing the parameter as Lengthy directly.
T extends X (a bounded type parameter) requires every binding of T to be assignable to X — which licenses the body to use X's members — while T still remembers the caller's exact type. Typing the parameter directly as X gets you the member access but erases the caller's specifics: longest(a: Lengthy, b: Lengthy): Lengthy returns the anonymous {length} shape even when called with two strings, whereas longest<T extends Lengthy>(a: T, b: T): T returns string. Constraint = demand the minimum capability; parameter = preserve the maximum information. Defaults (<T = string>) make the common case nicer to write for the common instantiation.
AppliedExplain function getProp<T, K extends keyof T>(obj: T, key: K): T[K] piece by piece, and name real systems built on this pattern.
T — the object's type, inferred from obj. K extends keyof T — K must be one of T's actual keys; keyof produces the literal union of key names (3.7.2), so passing "email" to a User without one is a compile error, and K binds to the specific literal passed ("age", not string). T[K] — indexed access: the type stored at that key, so the return type is exactly right per call (number for "age"). The whole signature: given any object and a proven key of it, you receive precisely that property's type. Systems built on it: typed ORMs/query builders (where("age", …) typed per column), form libraries (field name → field value type), pluck/groupBy/lens utilities, i18n key lookups, and the event-map pattern (on<K extends keyof Events>(name: K, cb: (e: Events[K]) => void)) used across DOM and Node typings.
InterviewWhen are function overloads the right tool, what are their pitfalls, and what should you try first?
Right tool when the return type genuinely depends on the shape of the call in ways one signature can't express — createElement("div") → HTMLDivElement, a range(end) vs range(start, end) API, string-vs-buffer duals in Node. Pitfalls: resolution walks the ordered list and takes the first match (misordered lists silently pick a too-general overload); the implementation signature is invisible to callers and only loosely checked against the body, so overload lists drift from reality; and long lists are a maintenance tax. Try first: (1) a union parameter + narrowing when the return type doesn't vary; (2) a constrained generic when it varies with an input type; (3) the key-lookup generic create<K extends keyof TagMap>(tag: K): TagMap[K] which replaces entire overload families with one relationship — the DOM typings' actual technique; (4) conditional types (3.7.5) for the residue. Overloads are the fallback for shape-dependent returns, not the default.
StaffA teammate reports: assigning a (msg: WelcomeEvent) => void handler where (msg: Event) => void is expected fails under strictFunctionTypes, yet an Array of WelcomeEvent assigns fine to Array of Event. Explain both behaviors from variance, the soundness hole the array case implies, and the API guidance you'd derive.
Both are the producer/consumer law. The handler consumes its parameter, so function types are contravariant in parameters under strictFunctionTypes: a (msg: Event) => void may substitute where (msg: WelcomeEvent) => void is expected (it handles anything), but the teammate's direction — specific-consumer into general slot — is unsafe: the slot promises to accept any Event, and the handler would receive, say, an ErrorEvent and read .user off it. The fix is to widen the handler's parameter (accept Event, narrow inside via a discriminant — 3.7.3) or use a per-event-key registration API (on<K extends keyof Events>). The array case assigns because TypeScript treats mutable arrays as covariant — pragmatic, but unsound since arrays also consume: pass your WelcomeEvent[] as Event[] and the callee may push(new ErrorEvent(…)) into it; your original alias now holds a lie, and the crash surfaces far away. (Methods' deliberate bivariance is what keeps this pleasant to useally tolerable.)
API guidance: accept readonly T[] for inputs (produce-only ⇒ soundly covariant, and documents non-mutation); make callback parameters as wide as the domain truly is and let handlers narrow; on generic interfaces you author, declare in/out so consumers get variance errors early with clear messages; and when a variance error appears, ask "who produces, who consumes" before reaching for as — the answer is the fix.
Flashcards
FlashGeneric vs any
any discards type info; a generic parameterizes and PRESERVES the input→output relationship, checked per call via inference.
FlashConstraint mantra
T extends X = demand the minimum (X's members usable), preserve the maximum (caller's exact T returned).
FlashThe property-access trio
T + K extends keyof T + T[K] → checked keys, exact value types. Backbone of ORMs, form libs, event maps.
FlashOverloads
Ordered signatures, first match wins, body loosely checked. Prefer unions / constrained generics / TagMap[K] lookups first.
FlashVariance law
Producers covariant (subtype flows along), consumers contravariant (flows backward). Mutable arrays: unsoundly covariant; readonly arrays: soundly.
FlashReturn-only generic
load<T>(url): Promise<T> — nothing to infer from ⇒ every call is an unchecked cast. Earn the type: pass the schema.
Scenario Drill
DrillDesign the types for a tiny typed event emitter: emitter.on(name, handler) and emitter.emit(name, payload) where each event name has its own payload type, wrong pairings must not compile, and a wildcard onAny(handler) receives every event. Walk the design and the variance decision in onAny.
Start from a payload map — the single source of truth:
typescript
interface Events {
login: { user: string; at: number };
logout: { user: string };
error: { code: number; message: string };
}
class Emitter<E extends Record<string, unknown>> {
on<K extends keyof E>(name: K, handler: (payload: E[K]) => void): void { /* … */ }
emit<K extends keyof E>(name: K, payload: E[K]): void { /* … */ }
onAny(handler: (name: keyof E, payload: E[keyof E]) => void): void { /* … */ }
}
const bus = new Emitter<Events>();Why it works: on/emit use the property-access trio — K extends keyof E pins the name to a real event and binds the literal ("login"), E[K] makes the payload exactly that event's shape; bus.emit("login", { user: "a" }) fails (missing at), bus.on("logni", …) fails (typo), and handlers need no annotations — inference delivers the payload type. The class is generic over the whole map (E), so one implementation serves every domain; consumers declare their Events interface and — because it's an interface — feature modules can extend it via declaration merging (3.7.6) to register their own events.
The onAny variance decision: the tempting signature handler: (payload: E[K]) => void for "some K" doesn't exist — there's no single K. The honest type is the union E[keyof E]: the wildcard handler consumes any payload, and consumers must be typed at least as wide as everything they can receive (contravariance — section 5). Inside, the handler narrows via the name parameter; make it nicer to use by passing a discriminated pair instead — (e: { [K in keyof E]: { name: K; payload: E[K] } }[keyof E]) => void (a mapped-union from 3.7.5) — so if (e.name === "login") narrows e.payload automatically.
What you deliberately did not do: overloads per event (the map generic replaces the whole family); any in the wildcard (the union + narrowing keeps it checked); a string enum of names (the keyof union derives names from the map — one source, zero drift). The drill's takeaways: model the map, derive names with keyof, pin payloads with E[K], and let variance dictate the wildcard's width.