Reference · type-level programming

Type-level programming — the Module 3 decoder

Mapped types loop over keys; conditional types branch; infer captures; template literals parse. Find the shape first; the behavior follows.

Mapped types & indexed access

shapereads asnote
keyof Tunion of T's key namesstring | number | symbol at most
T[K]value type at key Kindexed access
T[keyof T]union of all value typesindex by the whole key union
{ [K in keyof T]: … }for-each over keys → objectK 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

Key remapping with as

Mapped-type as is a different keyword from the value-world cast x as Type. It rewrites the output key: iterate → as remap → value.
goalclause
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).

Conditional types + infer

shapereads as
T extends U ? X : Ytype-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;

Distributive conditionals (and the off-switch)

A naked T in extends position distributes over a union: T extends U ? X : Y with T = A | B becomes (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"

Template-literal parsing

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"]

Record → discriminated union (flatten idiom)

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.

The 4-second read

  1. { [K in keyof T] … } → mapped loop; name the key set, substitute one key.
  2. as after in … → key remap (rename, or ? never : to drop).
  3. extends … ? … : … → conditional; infer R means capture a new type.
  4. naked T on a union → distributes; [T] extends [U] → does not.
  5. trailing }[keyof T] → flatten the mapped object into a union of its values.