TypeScript Generics Mastery
Move beyond Array<T> and learn the patterns that make TypeScript libraries elegant.
Constrained Generics
- function pluck<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
- return keys.reduce((acc, key) => { acc[key] = obj[key]; return acc; }, {} as Pick<T, K>);
- }
The K extends keyof T constraint does two jobs: it rejects invalid keys at the call site, and it lets the compiler *narrow* the return type — pluck(user, ['id']) returns a type with only id, not a vague partial. Constraints are how you tell the compiler "I will use this capability", and they should be as loose as possible while still permitting the function body: constraining to { length: number } accepts strings, arrays, and typed arrays alike.
Inference: The Feature Users Never See
- function pipe<A, B, C>(a: A, ab: (a: A) => B, bc: (b: B) => C): C { return bc(ab(a)); }
TypeScript infers all three parameters from the call site; users never write angle brackets. Good generic design is mostly *inference design*. Rules of thumb that follow from how inference works:
- Inference flows from arguments to type parameters, left to right; put the value that determines T before the callbacks that consume it, or callbacks get unknown parameters
- A type parameter that appears only once in a signature does nothing — function log<T>(x: T): void is just (x: unknown) => void with extra ceremony
- A parameter used only in the return type is a lie to the compiler: fetchJson<T>(url): Promise<T> performs no validation; it is a cast wearing a nicer syntax. Pair it with a runtime validator or accept that you wrote as T
When you need a literal type preserved, const type parameters (TS 5.0) fix the classic widening problem: function tuple<const T extends readonly unknown[]>(...args: T): T infers ['a', 'b'] as a tuple of literals rather than string[].
Conditional Types and infer
- type Awaited2<T> = T extends Promise<infer U> ? U : T;
infer pattern-matches a type and extracts a position. This is the mechanism behind built-ins like ReturnType, Parameters, and Awaited. The sharp edge is distribution: a conditional type applied to a *naked* type parameter distributes over unions. Exclude<'a' | 'b', 'a'> works because of this — the check runs per union member. When distribution is wrong for your case, wrap both sides in tuples: [T] extends [string] treats the union as one unit. Forgetting this is the number one source of "why did my conditional type return a union of both branches".
Also remember never is the empty union: a distributive conditional over never evaluates to never without ever running your branches — useful for filtering, baffling when unexpected.
Variadic Tuples
- type Last<T extends unknown[]> = T extends [...unknown[], infer L] ? L : never;
Variadic tuple types describe head/tail structure, which is what typed pipe, curry, and event-emitter signatures need. Combined with labeled tuple elements, they let a wrapper preserve the exact parameter list of the wrapped function: (...args: Parameters<F>) => ReturnType<F>.
Mapped Types and Key Remapping
Generics compose with mapped types to transform whole shapes:
- type Methods<T> = { [K in keyof T as T[K] extends (...a: never) => unknown ? K : never]: T[K] };
Key remapping with as — optionally combined with template literal types such as Capitalize to build names like getName from name — generates derived APIs — getters, event maps, form state — from one source of truth, so adding a field updates every derived type automatically.
Branded Types
- type UserId = string & { readonly __brand: 'UserId' };
- function asUserId(s: string): UserId { return s as UserId; }
Structural typing means any string fits any string parameter — until you brand it. The intersection with a phantom __brand property exists only at compile time (nothing is added at runtime) and makes UserId and OrderId mutually incompatible, catching swapped-argument bugs. Centralize the as casts in a few constructor functions that validate.
Common Mistakes
- Reaching for any inside generic function bodies to silence errors — use constraints or well-placed as on one line, not a type system opt-out
- Over-constraining (T extends object when T would do), which rejects valid callers for no body-level benefit
- Deeply recursive conditional types for problems a union and two overloads solve — compile times and error messages both suffer
- Trusting return-only type parameters as if they validated data
When to Stop
A generic signature that needs three lines of conditional types to read is usually a sign to split the type or accept a small amount of duplication. Optimize for the call site: the best generic code is the kind whose users never notice the angle brackets at all.