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 are checked against the list of overloads, from top to bottom, and the first match wins. So order matters: put the most specific first. The implementation signature is hidden from callers.
Honest guidance: overloads are the right tool when the return type really changes with the shape of the call, like the DOM's createElement("div") returning HTMLDivElement. But they are wordy, and the body is only loosely checked against them. So before writing overloads, try these first:
- a union parameter;
- a generic with a constraint;
- for lookup-style APIs like
createElement, a generic over a key:function create<K extends keyof TagMap>(tag: K): TagMap[K]. That one line replaces dozens of overloads.
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 rules, which looked arbitrary, start to make sense.
Arrays you can change both hand values out and take values in. Strict typing would therefore make Dog[] unrelated to Animal[]. TypeScript chooses convenience over strictness and lets a Dog[] be used as an Animal[] anyway. That leaves one hole: through the Animal[] name, someone can push an Animal that is not a dog into your Dog[]. A readonly T[] only hands values out, so for it the rule is truly safe. That is one more reason to use it for parameters.
strictFunctionTypes (3.7.1) checks function parameters in the safe, reversed direction for variables that hold functions. But method signatures still accept either direction. That leniency is on purpose: without it, Array<Dog> could not be used as an Array<Animal> at all.
Finally, in and out, as in interface Producer<out T> and interface Consumer<in T>, let library authors state the direction, and the compiler checks it against how the type is used. They are the same out and in you will 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:
- Things that hand out
Tare covariant: a narrower type can stand in for a wider one. - Things that take in
Tare contravariant: it goes the other way.strictFunctionTypesenforces this for function types, but methods accept either direction. - Arrays you can change are treated as covariant for convenience, which is not fully safe.
readonly T[]is safe. inandoutlet you state the direction.
- Things that hand out
- 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. Many tools are built on it:
- typed ORMs and query builders, where
where("age", …)is typed per column; - form libraries, where each field name maps to its value's type;
- helpers such as
pluckandgroupBy; - lookups of translation keys;
- the event-map pattern,
on<K extends keyof Events>(name: K, cb: (e: Events[K]) => void), used all over the DOM and Node type definitions.
InterviewWhen are function overloads the right tool, what are their pitfalls, and what should you try first?
Overloads are the right tool when the return type really depends on the shape of the call, in a way one signature cannot say. Examples: createElement("div") returning HTMLDivElement, an API with both range(end) and range(start, end), and Node functions that return a string or a buffer depending on the arguments.
The pitfalls:
- TypeScript checks the overloads in order and takes the first match. If they are in the wrong order, it quietly picks one that is too general.
- The implementation signature is hidden from callers, and only loosely checked against the body. So the list of overloads drifts away from what the code really does.
- Long lists are costly to maintain.
Try these first:
- A union parameter plus narrowing, when the return type does not change.
- A constrained generic, when the return type changes with an input type.
- A key-lookup generic,
create<K extends keyof TagMap>(tag: K): TagMap[K]. It replaces a whole family of overloads with one relationship, and it is what the DOM type definitions actually do. - Conditional types (3.7.5) for whatever is left.
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 come from the same rule about producers and consumers.
The handler case. A handler takes in its parameter. So under strictFunctionTypes, function parameters are contravariant. A (msg: Event) => void can stand in where a (msg: WelcomeEvent) => void is expected, because it can handle any event. But your teammate went the other way: a handler for one specific event, placed where a handler for any Event is expected. That is unsafe. The slot promises it will accept any Event, so the handler might receive an ErrorEvent and try to read .user from it.
The fix is to widen the handler's parameter: accept Event, then narrow inside using a discriminant (3.7.3). Or use a registration API keyed by event name, on<K extends keyof Events>.
The array case compiles because TypeScript treats arrays you can change as covariant. That is practical, but not fully safe, because arrays also take values in. Pass your WelcomeEvent[] as an Event[], and the function you called may push(new ErrorEvent(…)) into it. Your original variable now holds something its type says it cannot, and the crash appears somewhere far away. (Methods are deliberately allowed in either direction. That is what keeps everyday code like this bearable.)
Guidance for APIs:
- Accept
readonly T[]for inputs. A read-only array only hands values out, so it is safe to pass in an array of a narrower type. It also tells callers you will not change it. - Make callback parameters as wide as the real data can be, and let the handlers narrow them.
- On generic interfaces you write, declare
inorout, so users get variance errors early, with clear messages. - When a variance error appears, ask "who produces the value, and who consumes it?" 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 and emit use the three-part property-access trick:
K extends keyof Eforces the name to be a real event, and remembers the exact name passed, such as"login".E[K]makes the payload exactly that event's shape.- So
bus.emit("login", { user: "a" })fails, becauseatis missing.bus.on("logni", …)fails, because of the typo. And handlers need no type annotations, because TypeScript works out the payload type.
The class is generic over the whole map E, so one implementation works for every part of the app. Each part declares its own Events interface. Because it is an interface, feature modules can add to it through 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.