Conditional & Mapped Types
extends, infer, distribution, mapped key remapping — TypeScript at type level.
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:
- Hover everything. TS's hover almost always shows the resolved type.
type R = Foo<string | number>— hoverRand you see the answer. - Substitute mentally. Read
T extends X ? Y : Zby 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- serviceType Challengesfree tier
Browser-based type-puzzle exercises ramping from easy to fiendish.
- librarytype-festfree tier
Real-world conditional/mapped utilities you can read for technique.
- serviceTypeScript Playgroundfree tier
Hover any expression to see TS's resolved type — essential for debugging conditionals.