Part 3 · 2 chapters · ~25 min
Conditional, Mapped and Template Types
Conditional types as the if of the type language with distribution over unions, the tuple wrapper that stops it, infer as pattern matching and constrained infer, then mapped types as the loop over keys with modifiers and as-remapping, template literal types that compute and parse strings, recursion, the depth and size limits, and what type-level programming is for.
7
Conditional types: distribution and infer
the if of the type language
- The form:
T extends U ? X : Ywith assignability as the test (part 0), evaluated lazily when T is concrete; a conditional over an unresolved parameter stays deferred in hovers. - Distribution: with a naked type parameter and a union argument, the conditional maps over each member and unions the results (
ToArray<string | number>isstring[] | number[]); never in a branch drops the member, which is how Exclude and Extract filter. Most surprises are not knowing distribution is on. - Stopping it: wrap both sides in a tuple (
[T] extends [U]) to ask about the union as a whole;IsNeverneeds it because never distributes to nothing. - infer is pattern matching: bind what sits in a position of the shape (ReturnType, Parameters, an element type, a property, a template literal segment); same-name sites unify; Awaited shows recursion through PromiseLike.
- Constrained infer (
infer H extends string, since 4.7) keeps the branch typed without a nested conditional: the pattern for head/tail recursion over tuples and for template parsing. - The built-ins in lib.es5.d.ts are a few lines each and the textbook: Partial, Required, Readonly, Pick and Record are mapped; Exclude, Extract, NonNullable, ReturnType, Parameters, InstanceType and Awaited are conditionals. The limits: deep nesting is slow to check and its errors expand; a conditional in a hot generic costs on every call (part 5).
CONDITIONAL TYPES: DISTRIBUTION AND INFER
the if-statement of the type language, how it maps over unions, and how it pattern-matches
swipe the figure sideways, or tap expand for full screen
1/6
the form
The form: type IsString = T extends string ? true : false. IsString<"a"> is true; IsString is false. The check is assignability (part 0); the branches are any types; the whole thing is evaluated lazily when T is known, which is why a conditional over an unresolved parameter stays "deferred" in hovers and errors.
8
Mapped and template literal types
code
// type-level programming that earns its keep: a typed event map, a typed route, and a DTO shaper
type Events = { 'user:login': { id: string }; 'user:logout': { id: string; reason: 'timeout' | 'manual' }; 'cart:add': { sku: string; qty: number } }
// 1. mapped + template: handler names from event names; a typed emitter
type Handlers = { [K in keyof Events as `on${Capitalize<Replace<K, ':', '_'>>}`]: (p: Events[K]) => void }
type Replace<S extends string, A extends string, B extends string> = S extends `${infer H}${A}${infer T}` ? `${H}${B}${Replace<T, A, B>}` : S
declare function on<K extends keyof Events>(name: K, h: (p: Events[K]) => void): void
on('user:logout', p => p.reason) // p: { id: string; reason: 'timeout' | 'manual' }; a typo in the name is an error
// 2. template + infer + recursion: path params from a route string
type Params<P extends string> = P extends `${string}:${infer Name}/${infer Rest}` ? { [K in Name | keyof Params<Rest>]: string }
: P extends `${string}:${infer Name}` ? { [K in Name]: string } : {}
declare function route<P extends string>(path: P, handler: (params: Params<P>) => void): void
route('/users/:userId/posts/:postId', ({ userId, postId }) => {}) // both typed; a third is an error
// 3. conditional + mapped: a DTO with only serialisable fields, readonly, dates as strings
type Serialisable<T> = { readonly [K in keyof T as T[K] extends Function ? never : K]: T[K] extends Date ? string : T[K] extends object ? Serialisable<T[K]> : T[K] }
type UserDto = Serialisable<{ id: string; createdAt: Date; save(): void; profile: { born: Date } }>
// → { readonly id: string; readonly createdAt: string; readonly profile: { readonly born: string } }
// the test for each: hover the result. if the hover is unreadable, the type is too clever for the team that will maintain itthe loop, the string computation, and the limits
- The loop over keys:
{ [K in keyof T]?: T[K] }with modifiers added or removed (-?,-readonly); homomorphic mapped types (overkeyof T) preserve modifiers and array-ness;Recordis the non-homomorphic form. - Key remapping with as: compute new keys through a template literal (getters from properties); never in the as clause drops a property (a filter by value type); Capitalize and friends are intrinsic.
- Template literal types compute strings and distribute per position (a cross product); infer over them parses strings at the type level: typed routers and path params are this.
- Recursion in a deferrable position (an element, a property, a branch): trees (Json) and lists (Flatten over tuples); a top-level self-reference errors; the depth limit is about a thousand instantiations with tail-call optimisation for tail-recursive conditionals since 4.5.
- Limits and cost: a mapped type over a thousand keys is a thousand property types; a template cross product of three unions of ten is a thousand strings; recursion is depth times width; "type instantiation is excessively deep" and the 100,000-member union limit are hard errors. Instantiations are cached, but a hot generic over a large object costs in every file that uses it (part 5).
- What for: typed APIs from a schema (routes, events, keys: the FSD course M4's query layer, the Big-company FE course part 3's generated types) and your own domain's utilities; never for runtime logic (validation of user input runs at runtime, the Trust course's parsers; a type cannot check a string the user typed). The smell test is whether a hover is legible to the team that will maintain it.
the exercise
Read Partial, Exclude, ReturnType and Awaited in lib.es5.d.ts, then write
DeepReadonly<T> and a PathParams<P> for your router. Hover each result; if you cannot read it, simplify until you can.MAPPED AND TEMPLATE LITERAL TYPES
the for-loop over keys, remapping with as, template literals as string computation, and recursion with its limits
swipe the figure sideways, or tap expand for full screen
1/6
the loop
The loop: type Partial = { [K in keyof T]?: T[K] }; Required adds -? ; Readonly adds readonly; Mutable removes it with -readonly. Homomorphic mapped types (over keyof T) preserve modifiers and array-ness; a mapped type over an arbitrary key union does not. Record = { [P in K]: V } is the non-homomorphic form.