On this page

Conditional and mapped types

14 min read TextCh. 4 — Advanced Types

The shape of advanced type manipulation

In the previous lessons you used the built-in utility types. In this lesson you learn how those utilities are built — and how to write your own. The two key ingredients are conditional types and mapped types. Together with template literal types, they give you a complete language for transforming types at compile time.

Conditional types

A conditional type has the form T extends U ? X : Y. If T is assignable to U, the whole expression resolves to X; otherwise it resolves to Y.

type IsArray<T> = T extends unknown[] ? true : false;

type A = IsArray<number[]>;  // true
type B = IsArray<string>;    // false

Conditional types compose naturally with generics:

type Unwrap<T> = T extends Promise<infer V>
  ? V
  : T extends Array<infer V>
    ? V
    : T;

type A = Unwrap<Promise<string>>;  // string
type B = Unwrap<number[]>;         // number
type C = Unwrap<boolean>;          // boolean

Distributive behavior

When the checked type is a naked (un-wrapped) type parameter, TypeScript automatically distributes the conditional over each union member:

type WrapInArray<T> = T extends unknown ? T[] : never;

type Result = WrapInArray<string | number>;
// string[] | number[]  — distributed over the union

To prevent distribution, wrap the parameter in a one-element tuple:

type NonDistributive<T> = [T] extends [unknown] ? T[] : never;

type Result = NonDistributive<string | number>;
// (string | number)[]  — the entire union becomes the element type

Mapped types

A mapped type iterates over a union of keys and produces a new object type:

type ReadonlyVersion<T> = {
  readonly [K in keyof T]: T[K];
};

This is exactly how the built-in Readonly<T> is implemented. The [K in keyof T] syntax means "for each key K in the keys of T". You can add or remove modifiers:

// Remove readonly and optional modifiers
type Mutable<T> = {
  -readonly [K in keyof T]-?: T[K];
};

The - prefix before readonly or ? removes that modifier. The + prefix (default) adds it.

Mapping over a custom union

Mapped types are not limited to keyof T. You can iterate over any string (or number/symbol) union:

type StatusColors = "success" | "warning" | "error" | "info";

type AlertStyles = {
  [S in StatusColors]: {
    background: string;
    icon: string;
  };
};

Key remapping with `as`

TypeScript lets you rename keys during a mapping using the as clause:

type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

interface Profile {
  name: string;
  age: number;
}

type ProfileSetters = Setters<Profile>;
// { setName: (value: string) => void; setAge: (value: number) => void }

You can also use as with a conditional to filter out keys by remapping them to never:

type PickByValue<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K];
};

interface Mixed {
  id: number;
  name: string;
  active: boolean;
  score: number;
}

type NumberFields  = PickByValue<Mixed, number>;  // { id: number; score: number }
type StringFields  = PickByValue<Mixed, string>;  // { name: string }

Template literal types

Template literal types let you build new string literal types by combining existing ones, using the same syntax as JavaScript template literals:

type HttpMethod = "get" | "post" | "put" | "delete";
type ApiPath    = "/users" | "/posts" | "/comments";

type Endpoint = `${Uppercase<HttpMethod>} ${ApiPath}`;
// "GET /users" | "GET /posts" | ... (12 combinations total)

Combined with mapped types, this enables powerful API-contract modeling:

type ApiClient = {
  [M in HttpMethod]: (path: string, body?: unknown) => Promise<unknown>;
};

Building a custom `DeepPartial`

The built-in Partial<T> only goes one level deep. Here is a recursive version using conditional and mapped types:

type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

interface AppState {
  user: {
    profile: {
      name: string;
      bio: string;
    };
    settings: {
      theme: "dark" | "light";
      notifications: boolean;
    };
  };
}

type PartialState = DeepPartial<AppState>;

// All nested properties are optional:
const patch: PartialState = {
  user: {
    settings: {
      theme: "dark",
      // notifications omitted — valid!
    },
  },
};

Building a `Paths` type

A more advanced example — computing all dot-notation paths of a nested object:

type Paths<T, Prefix extends string = ""> = {
  [K in keyof T & string]: T[K] extends object
    ? Paths<T[K], `${Prefix}${K}.`> | `${Prefix}${K}`
    : `${Prefix}${K}`;
}[keyof T & string];

interface Form {
  user: {
    name: string;
    address: {
      city: string;
      zip: string;
    };
  };
}

type FormPaths = Paths<Form>;
// "user" | "user.name" | "user.address" | "user.address.city" | "user.address.zip"

This is the foundation of libraries like React Hook Form and Zod that provide type-safe field paths.


Practice

  1. Basic mapped type: Write a Nullable<T> mapped type that makes every property of T accept null in addition to its original type. Test it on a User interface.
  2. Key remapping: Write a EventHandlers<T> type that, given an interface, produces a new interface where each key k is renamed to onK (capitalized) and each value type becomes (payload: OriginalValueType) => void.
  3. Conditional type: Write IsPromise<T> — a conditional type that resolves to true if T is a Promise<unknown>, and false otherwise. Test it with Promise<string>, string, and Promise<number[]>.
  4. Template literal: Define a union of REST resource names ("user" | "post" | "comment"). Use a mapped type with a template literal to produce a record type where keys are getUsers, getPosts, getComments (pluralized and prefixed with get) and values are () => Promise<unknown>.

Next lesson: classes and decorators — object-oriented TypeScript and the TC39 decorator syntax.

Start with the simplest utility types before writing your own
Before building a custom conditional or mapped type, verify that `Partial`, `Pick`, `Omit`, `Record`, `Extract`, or `Exclude` (covered in the previous lesson) cannot already solve your problem. Custom meta-types are powerful but they add cognitive overhead for every developer who reads your code.
Capitalize, Uppercase, Lowercase, Uncapitalize are built-in
TypeScript provides four intrinsic string manipulation types for use with template literals. `Capitalize<S>` uppercases the first character; `Uncapitalize<S>` lowercases it; `Uppercase<S>` and `Lowercase<S>` transform the entire string.
Recursive mapped types can cause deep instantiation errors
When you write recursive types like `DeepReadonly`, TypeScript limits recursion depth to avoid infinite loops. For extremely nested structures you may hit the "Type instantiation is excessively deep" error. Prefer shallower alternatives or libraries like `ts-essentials` for production use.
typescript
// Basic mapped type — iterate over every key of T
type Optional<T> = {
  [K in keyof T]?: T[K];
};

// Key remapping with "as" — rename keys
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface User {
  id: number;
  name: string;
  email: string;
}

type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getEmail: () => string }

// Filter keys with "as" + conditional (remove never to exclude keys)
type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type StringProps = OnlyStrings<User>;
// { name: string; email: string }   (id excluded — it's a number)
typescript
// Template literal types — compose string types
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

// Full event map using mapped + template literal
type EventMap = {
  [E in EventName as `on${Capitalize<E>}`]: (event: Event) => void;
};
// { onClick: ...; onFocus: ...; onBlur: ... }

// Custom DeepReadonly using conditional + mapped types
type DeepReadonly<T> = T extends (infer U)[]
  ? ReadonlyArray<DeepReadonly<U>>
  : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T;

interface Config {
  server: { host: string; port: number };
  flags: string[];
}

type ImmutableConfig = DeepReadonly<Config>;
// server.host and server.port are readonly, flags is ReadonlyArray<string>

// Distributive conditional — flatten or wrap union members
type Nullable<T> = T extends unknown ? T | null : never;
type IdsOrNull = Nullable<number | string>;
// number | null | string | null  =>  number | string | null