SyntaxStudy
Sign Up
TypeScript Conditional Types and Mapped Types Deep Dive
TypeScript Beginner 1 min read

Conditional Types and Mapped Types Deep Dive

Conditional types and mapped types are the two most powerful tools in TypeScript's type-level programming toolkit. When combined, they allow you to express transformations that would be impossible to describe any other way. Many of TypeScript's built-in utility types are implemented using exactly this combination, and studying their source code is one of the best ways to master these features. Distributive conditional types automatically apply the condition to each member of a union when the checked type is a naked type parameter. This behaviour is usually what you want, but it can be suppressed by wrapping the type parameter in a tuple: [T] extends [U]. The non-distributive form is essential for writing conditional types that should treat a union as a single entity rather than branching over its members. Mapped types iterate over a union of string literals (usually from keyof) and produce a new object type. The combination of key remapping (as), conditional filtering (using never to drop keys), and value transformation enables sophisticated type operations. Understanding how to chain these operations and read the resulting types is a key skill for TypeScript experts.
Example
// Distributive vs non-distributive conditional types
type IsArray<T> = T extends unknown[] ? true : false;
type DA = IsArray<string | number[]>; // boolean (distributive)

type IsArrayND<T> = [T] extends [unknown[]] ? true : false;
type NDA = IsArrayND<string | number[]>; // false (non-distributive, whole union)

// Deep mapped type with conditional branching
type Serialize<T> =
    T extends Date             ? string :
    T extends (infer U)[]      ? Serialize<U>[] :
    T extends object           ? { [K in keyof T]: Serialize<T[K]> } :
    T;

interface Event {
    id: number;
    name: string;
    date: Date;
    tags: Date[];
    meta: { createdAt: Date };
}

type SerializedEvent = Serialize<Event>;
// { id: number; name: string; date: string; tags: string[]; meta: { createdAt: string } }

// Flattening nested types
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
type F1 = Flatten<number[][][]>; // number

// Type-level function composition
type Compose<F, G> =
    F extends (arg: infer A) => infer B
    ? G extends (arg: B) => infer C
    ? (arg: A) => C
    : never : never;

type StrToNum = (s: string) => number;
type NumToBool = (n: number) => boolean;
type StrToBool = Compose<StrToNum, NumToBool>; // (arg: string) => boolean