Reference · type-level programming
Mapped types loop over keys; conditional types branch; infer captures;
template literals parse. Find the shape first; the behavior follows.
| shape | reads as | note |
|---|---|---|
| keyof T | union of T's key names | string | number | symbol at most |
| T[K] | value type at key K | indexed access |
| T[keyof T] | union of all value types | index by the whole key union |
| { [K in keyof T]: … } | for-each over keys → object | K is the current key |
type MyPartial<T> = { [K in keyof T]?: T[K] }; // { a:1 } → { a?:1 }
type Getters<T> = { [K in keyof T]: () => T[K] }; // each value → 0-arg fn
asMapped-typeasis a different keyword from the value-world castx as Type. It rewrites the output key: iterate →asremap → value.
| goal | clause |
|---|---|
| rename / template key | [K in keyof T as `get${Capitalize<K & string>}`] |
| drop a key (filter) | [K in keyof T as K extends "pw" ? never : K] |
// rename — value still read from original key
type Getters<T> = { [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K] };
// filter — never as the KEY removes the property (never vanishes from a key union)
type Public<T> = { [K in keyof T as K extends "password" ? never : K]: T[K] };
K & string narrows keyof T to its string members so
Capitalize accepts it (keyof T can be number/symbol too — same reason
Record<string, any> still admits number keys).
infer| shape | reads as |
|---|---|
| T extends U ? X : Y | type-level if: assignable to U? X else Y |
| … extends … infer R … | declare + capture a new variable R from the match |
// assignability test (no capture)
type IsString<T> = T extends string ? "yes" : "no";
// capture: a bare R errors — infer DECLARES it inside extends
type ReturnType<T> = T extends (...a: any[]) => infer R ? R : any;
A nakedTinextendsposition distributes over a union:T extends U ? X : YwithT = A | Bbecomes(A extends U ? …) | (B extends U ? …).
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[] (NOT (string|number)[])
type Z = ToArray<never>; // never (empty union → zero iterations)
// wrap both sides in [ ] to turn distribution OFF — test the union as one
type IsNever<T> = [T] extends [never] ? "yes" : "no"; // IsNever<never> → "yes"
A template pattern with infer splits a string and binds each hole. Recurse on the
captured tail; give a base case or TS errors on infinite depth.
type GetParams<S extends string> =
S extends `${string}{${infer Name}}${infer Rest}`
? [Name, ...GetParams<Rest>] // keep Name, recurse on Rest
: []; // base case: no more {…}
// GetParams<"/users/{id}/posts/{postId}"> → ["id", "postId"]
type ToUnion<T extends Record<string, any>> = {
[K in keyof T]: { type: K } & T[K]; // 1. tag each payload → object
}[keyof T]; // 2. index by all keys → union of values
// { click:{x:number}; keydown:{key:string} }
// → | { type:"click"; x:number }
// | { type:"keydown"; key:string }
The trailing }[keyof T] is the flatten: object keyed by name → union of its values. Drop
it and you keep the object.
{ [K in keyof T] … } → mapped loop; name the key set, substitute one key.as after in … → key remap (rename, or ? never : to drop).extends … ? … : … → conditional; infer R means capture a new type.T on a union → distributes; [T] extends [U] → does not.}[keyof T] → flatten the mapped object into a union of its values.