Reading — step 1 of 5
Learn
~1 min readType-Level Programming
Mapped types transform an object type by iterating its keys:
type Nullable<T> = { [K in keyof T]: T[K] | null };
interface User { name: string; age: number; }
type NullableUser = Nullable<User>;
// { name: string | null; age: number | null }
The [K in keyof T] part loops over each key of T. T[K] is the value type at that key.
Modifiers add or remove readonly / ?:
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] }; // strip optional
Conditional types — type-level if:
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
infer captures a type variable from a pattern:
type ElementOf<T> = T extends (infer U)[] ? U : never;
type A = ElementOf<number[]>; // number
type B = ElementOf<string[]>; // string
This is how ReturnType<F> is implemented:
type ReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
Distributing over unions — conditionals over a union type apply per-variant:
type StringOnly<T> = T extends string ? T : never;
type A = StringOnly<"a" | 42 | "b">; // "a" | "b"
This enables features like the built-in Exclude<T, U> and Extract<T, U>. Type-level programming gets weird fast — but a few tricks like ReturnType and ElementOf cover 90% of practical cases.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…