Skip to content
Mapped and Conditional Types
step 1/5

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…