typescript · level 7

Conditional & Mapped Types

extends, infer, distribution, mapped key remapping — TypeScript at type level.

200 XP

Conditional & Mapped Types

Conditional and mapped types are the type-level programming language inside TypeScript. Most TypeScript code never touches them. The 5% of code that does — utility types, generic schemas, type-safe ORMs — uses them constantly. Two new pieces of syntax, both small, both worth learning carefully.

Conditional types: ternary at the type level

type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<"hello">;   // "yes"
type B = IsString<42>;        // "no"

Read it: "if T is assignable to string, the result is \"yes\"; otherwise \"no\"." Same shape as a value-level cond ? a : b, but operates on types.

The extends here is assignability — "is T a subtype of X?" — not class extension. Two literal types "hello" and string pass extends because every literal-string is also a string.

Distribution

Here's where conditional types diverge from value ternaries. When the type parameter T is naked (used directly, not wrapped) and you pass it a union, the conditional distributes:

type Foo<T> = T extends string ? "s" : "n";
type R = Foo<string | number>;
//   ^? "s" | "n"        ← evaluated for each member

TypeScript runs the conditional separately for each member of the union: Foo<string>"s", Foo<number>"n", then unions the results.

This is incredibly powerful — and a constant source of confusion when you didn't want it.

To opt out of distribution, wrap T in a tuple:

type FooN<T> = [T] extends [string] ? "s" : "n";
type R2 = FooN<string | number>;
//   ^? "n"            ← single answer; no distribution

The wrapping [T] makes T no longer naked, so the rule doesn't fire. [string | number] is not assignable to [string], so the conditional returns "n" once.

Memorise this trick. You'll need it when writing utility types that should treat unions as a single thing.

infer: capturing a piece of a matched type

infer X declares a type variable inside the extends clause. If the conditional matches, X gets bound to whatever filled that slot:

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

type A = Unwrap<Promise<{ ok: boolean }>>;   // { ok: boolean }
type B = Unwrap<number>;                     // number

Read it: "if T looks like Promise<something>, return the something; otherwise return T." Awaited<T>, ReturnType<F>, Parameters<F>, InstanceType<C> are all built on this pattern.

type ReturnType<F> = F extends (...args: any) => infer R ? R : never;
type Parameters<F> = F extends (...args: infer P) => any ? P : never;
type Awaited<T>    = T extends Promise<infer U> ? Awaited<U> : T;   // recursive

Recursive conditional types — like Awaited flattening Promise<Promise<X>> — are a TS 4.1+ feature. They're powerful but easy to make non-terminating, so use sparingly.

Mapped types: iterate over keys

type Optional<T> = { [K in keyof T]?: T[K] };

type U = { a: number; b: string };
type R = Optional<U>;   // { a?: number; b?: string }

Read [K in keyof T] as "for each key K in T, produce a property". The brackets are required syntax — they're how you say "computed key".

The modifiers you can add or remove:

type ReadOnly<T> = { readonly [K in keyof T]: T[K] };       // add readonly
type Mutable<T>  = { -readonly [K in keyof T]: T[K] };       // remove readonly
type Required<T> = { [K in keyof T]-?: T[K] };               // remove ?

+ and - are explicit modifier toggles. + is the default when adding (? and readonly); use - to strip them.

Key remapping with as

You can rewrite keys as you map:

type Prefixed<T> = {
  [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K]
};

type U = { a: number; b: string };
type R = Prefixed<U>;
//   ^? { getA: () => number; getB: () => string }

as declares the new key name. Combined with template-literal types (`get${...}`) and built-in helpers (Capitalize, Uppercase, Lowercase, Uncapitalize), you get extremely expressive renaming.

You can also filter keys by mapping unwanted ones to never:

type DataOnly<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K]
};

Keys whose value is a function get re-keyed to never, which TS drops from the resulting object. The result has only data fields.

Putting them together

The library type-fest (and similar) is a tour of these techniques. A small example:

// Make exactly one of a set of keys required, others stay optional.
type RequireExactlyOne<T, K extends keyof T = keyof T> = {
  [P in K]: Required<Pick<T, P>> & Partial<Record<Exclude<K, P>, never>>;
}[K];

That uses: mapped type, intersection, Pick + Required (utility composition), Partial + Record + Exclude (more utility composition), and the [K] indexed-access trick to flatten the result. Each piece is small. The combination expresses something concrete: "pass exactly one of these fields, no more, no less."

Reading conditional/mapped types

Two habits that make this readable:

  1. Hover everything. TS's hover almost always shows the resolved type. type R = Foo<string | number> — hover R and you see the answer.
  2. Substitute mentally. Read T extends X ? Y : Z by replacing T with the actual argument and asking the assignability question. Same with [K in keyof T] — pick a concrete T and walk through each key.

When to write your own

Reach for conditional/mapped when:

  • A built-in utility almost fits but you need to rename keys, filter values, or transform every property.
  • You're writing a library and need to expose a type-level transformation as part of the API.
  • You're modeling a generic data structure (a typed event emitter, a typed router) where each operation derives from a single source-of-truth schema.

Don't reach for them when:

  • You can express the same thing by hand. { a?: number; b?: string } is easier to read than a clever helper for two fields.
  • The result type is more confusing than the underlying logic. If the next person on your team can't quickly understand it, it's a liability.

The skill is restraint. Use just enough type-level machinery to make the runtime code obviously correct — no more.

Tools in the wild

3 tools
  • Browser-based type-puzzle exercises ramping from easy to fiendish.

    service
  • type-festfree tier

    Real-world conditional/mapped utilities you can read for technique.

    library
  • Hover any expression to see TS's resolved type — essential for debugging conditionals.

    service