typescript · memo
In one line: Type parameters are solved by inference at each use, bounded by
extends; conditional, mapped and template-literal types form a
small pure functional language the checker evaluates — erased at runtime, limited by
instantiation depth, and only as good as the inference it enables at call sites.
Download PDF Print view LaTeX source
How it works
- Constraint
<T extends HasId>= upper bound, like SwiftT: Protocolbut structural; default<T = string>. Rule: a type parameter must appear twice (links input to output), else useunknown. - Inference collects candidates from each argument; literals widen unless
T extends string(keeps'a') or<const T>(5.0 — infers as ifas const; arrays need areadonly unknown[]constraint).NoInfer<T>(5.4) removes one position from inference. keyof T: union of keys (index signaturestringgivesstring|number).typeof v: value → type.T[K]: indexed access;T[number]= element type.- Conditional
T extends U ? X : Y;infer Rcaptures a piece of the match. WithTstill generic it is deferred — inside a generic body you can’t narrow it, hence casts in implementations. - Template literals (4.1):
`on${Capitalize<K>}`, cross-product over unions,inferparses strings. Intrinsics:Uppercase,Lowercase,Capitalize,Uncapitalize. - Recursion:
type Json = … | Json[] | {[k: string]: Json}. Tail-recursive conditional types go ≈1000 deep (4.5); else error 2589 “instantiation is excessively deep and possibly infinite”. - Swift: generics there are specialised/witness-table dispatched, invariant (except built-ins), and have no conditional/mapped types.
some P≈ a genericT;any P≈ plain interface type (no box needed: structural).
Utility types from scratch (lib.es5.d.ts)
export {}; // module scope: shadows the global names
type Partial<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Record<K extends keyof any, V> = { [P in K]: V };
type Exclude<T, U> = T extends U ? never : T; // distributes
type Extract<T, U> = T extends U ? T : never;
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type NonNullable<T> = T & {}; // since 4.8
type ReturnType<T extends (...a: any) => any> =
T extends (...a: any) => infer R ? R : any;
type Parameters<T extends (...a: any) => any> =
T extends (...a: infer P) => any ? P : never;
type Awaited<T> = // simplified: real one handles any thenable
T extends PromiseLike<infer V> ? Awaited<V> : T;
Example — what seniors write
type Events = { login: { id: string }; logout: undefined };
type Handlers = { // onLogin, onLogout
[K in keyof Events as `on${Capitalize<string & K>}`]:
(e: Events[K]) => void };
function get<T, K extends keyof T>(o: T, k: K): T[K] {
return o[k]; }
type Params<S> = S extends `${string}:${infer P}/${infer R}`
? P | Params<`/${R}`>
: S extends `${string}:${infer P}` ? P : never;
type P1 = Params<'/u/:id/post/:pid'>; // 'id' | 'pid'
type DeepReadonly<T> = T extends (...a: any[]) => any ? T
: { readonly [K in keyof T]: DeepReadonly<T[K]> };
function fsm<S extends string>(states: S[], start: NoInfer<S>) {}
fsm(['on', 'off'], 'of'); // error thanks to NoInfer
interface Getter<out T> { get: () => T }
interface Setter<in T> { set: (v: T) => void }
Interview traps
- “
{a:1}is assignable to the constraint ofT, butTcould be instantiated with a different subtype” — you returned a new object where the caller’s exactTwas promised. Omit/keyofon a union keep only common keys: distribute first —T extends any ? Omit<T, K> : never.IsNever<T> = T extends never ? …yieldsneverfornever: write[T] extends [never].- Arrays are covariant only through method bivariance:
Dog[]asAnimal[]thenpush(cat)compiles. - Clever types cost compile time and error readability; prefer overloads or a plain union when a call site doesn’t gain inference.
Remember
Constrain, infer, then compute: conditional = if, mapped = for-each, infer =
pattern bind, recursion = loop.
Likely questions
- Distributive conditional? — a naked
Tmaps over each union member; wrap in[T]to stop it. - Covariant vs contravariant? — outputs keep the subtype arrow, inputs flip it, both = invariant.
- Why
NoInfer? — so a default/secondary argument can’t widenT. consttype parameter? — literal/readonly inference withoutas constat every call.- Implement
Pick/ReturnType? — see the block above.