cheat sheet
Mapped & Conditional Types
Mapped types iterate over type keys to create new types; conditional types pick between types based on a condition. Together they power all built-in utility types and advanced type composition.
Mapped & Conditional Types
What it is
Mapped types create new types by iterating over the keys of an existing type and transforming each key's type. Conditional types select between two types based on whether a type extends another. Used together, they are the building blocks behind every built-in utility type (Partial, Required, ReturnType, etc.) and most advanced type-level programming in TypeScript.
Mapped type syntax
A mapped type uses the [K in keyof T] syntax to iterate over each key of a source type and emit a new key/value pair. Think of it as a for loop over the type system: for every key K of T, produce some transformed key and value. The transformation can change value types, add/remove modifiers (readonly, ?), rename keys via as, or filter keys out by mapping them to never.
// ┌── new modifier prefix (optional)
// │ ┌── key variable
// │ │ ┌── what we iterate over
// │ │ │ ┌── new key (optional `as`)
// │ │ │ │ ┌── value type
// ▼ ▼ ▼ ▼ ▼
// +readonly [K in keyof T as NewKey<K>]: V<K>;
A mapped type uses the in keyof syntax to iterate over each key:
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Partial<T> = {
[K in keyof T]?: T[K];
};
type Stringify<T> = {
[K in keyof T]: string;
};
interface User {
id: number;
name: string;
active: boolean;
}
type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; readonly active: boolean }
type PartialUser = Partial<User>;
// { id?: number; name?: string; active?: boolean }
type StringifiedUser = Stringify<User>;
// { id: string; name: string; active: string }
Modifiers: readonly and optional
Mapped types can add or remove the readonly and ? (optional) modifiers using + or - prefix. The + form is the explicit "add this modifier" — it's the default and rarely written. The - form is the more interesting one: it strips an existing modifier, which is how Required<T> and a hand-rolled Mutable<T> work.
// Add readonly (same as just using 'readonly')
type Immutable<T> = { +readonly [K in keyof T]: T[K] };
// Remove readonly — Mutable<T>
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
// Add optional (same as just using '?')
type Nullable<T> = { [K in keyof T]+?: T[K] };
// Remove optional — Required<T>
type Required<T> = { [K in keyof T]-?: T[K] };
interface FrozenConfig {
readonly host: string;
readonly port: number;
}
type EditableConfig = Mutable<FrozenConfig>;
// { host: string; port: number } — readonly removed
interface PartialUser {
name?: string;
email?: string;
}
type FullUser = Required<PartialUser>;
// { name: string; email: string } — optionals removed
Combining modifiers
Multiple modifiers stack — you can simultaneously remove readonly and ? to produce a strict, mutable shape from a forgiving one:
type Concrete<T> = {
-readonly [K in keyof T]-?: T[K];
};
interface MaybeFrozen {
readonly id?: number;
readonly name?: string;
}
type Strict = Concrete<MaybeFrozen>;
// { id: number; name: string }
Concrete<T> is a useful local utility — it's exactly the inverse of Partial<Readonly<T>>.
+ vs no prefix
+? and ? are functionally identical, and +readonly and readonly are too. The + form documents intent — useful in library code where reviewers should immediately know the modifier is being added, not just inherited from the source type.
Mapping over union vs object
The in keyword has two productive forms: K in keyof T iterates over an object's keys, while K in SomeUnion iterates over the members of a union directly. The second is how you'd write a Record-from-scratch.
// Iterate over the keys of T (object source)
type Stringify1<T> = { [K in keyof T]: string };
// Iterate over a union directly — no object
type Stringify2<K extends string> = { [P in K]: string };
type R1 = Stringify1<{ a: number; b: boolean }>; // { a: string; b: string }
type R2 = Stringify2<"x" | "y">; // { x: string; y: string }
Record<K, V> is just { [P in K]: V } — a mapped type over the union K.
Key remapping with as
The as clause inside a mapped type renames or filters keys:
// Prefix all keys with "get"
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person { name: string; age: number }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
Filter keys by type using never to exclude them:
// Keep only keys whose value type is string
type StringKeys<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed {
id: number;
name: string;
active: boolean;
email: string;
}
type StringOnly = StringKeys<Mixed>;
// { name: string; email: string }
Rename keys using template literals:
type EventHandlers<T extends Record<string, unknown>> = {
[K in keyof T & string as `on${Capitalize<K>}Change`]: (value: T[K]) => void;
};
type FormHandlers = EventHandlers<{ name: string; age: number }>;
// { onNameChange: (value: string) => void; onAgeChange: (value: number) => void }
Conditional type syntax
A conditional type evaluates to one branch or another based on whether a type satisfies a constraint, following the ternary form T extends U ? X : Y. Use conditional types when you need a type to adapt its output depending on what is passed in — they are the foundation of utility types like NonNullable, ReturnType, and Awaited.
// T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true — string literal extends string
Practical conditional type — unwrap nullable:
type NonNullable<T> = T extends null | undefined ? never : T;
type E = NonNullable<string | null | undefined>; // string
type F = NonNullable<number | null>; // number
Conditional return based on input:
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type G = Flatten<string[]>; // string
type H = Flatten<number[][]>; // number[]
type I = Flatten<boolean>; // boolean (passthrough)
Distributive conditional types
A distributive conditional type is one whose left-hand side is a bare generic parameter (just T, not [T] or T & U). When such a conditional meets a union, the compiler runs the conditional once per union member and unions the results — this is exactly how Exclude and Extract work. The behaviour is so foundational that almost every utility type that operates on unions either depends on distribution or deliberately disables it.
Naked vs wrapped type variables
The terms "naked" and "wrapped" refer to the form of the conditional's check-type. A naked type variable (T extends ...) triggers distribution; a wrapped one ([T] extends [...], or T[] extends ..., etc.) does not. The single-tuple wrap is the standard idiom for "I want the union as-a-whole."
// Naked — distributes
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string | number>;
// "yes" | "no" — distributed once per union member
// Wrapped — opts out of distribution
type IsStringAll<T> = [T] extends [string] ? "yes" : "no";
type B = IsStringAll<string | number>;
// "no" — checks the union as a whole
type C = IsStringAll<string>;
// "yes"
When you want to test "is this entire union one specific shape?", wrap. When you want per-member behaviour, leave it naked.
When a conditional type is applied to a generic type parameter, it distributes over each member of a union:
type ToArray<T> = T extends any ? T[] : never;
type J = ToArray<string | number>;
// string[] | number[] — distributed!
// NOT (string | number)[]
// Without distribution (wrap in a tuple to prevent it):
type ToArraySingle<T> = [T] extends [any] ? T[] : never;
type K = ToArraySingle<string | number>;
// (string | number)[]
This distributive behavior powers Exclude and Extract:
type Exclude<T, U> = T extends U ? never : T;
// Distributes over T, removing members assignable to U
type L = Exclude<"a" | "b" | "c", "a" | "b">;
// "c"
// Expands to: ("a" extends "a"|"b" ? never : "a") | ("b" extends "a"|"b" ? never : "b") | ("c" extends "a"|"b" ? never : "c")
// = never | never | "c" = "c"
Distribution gotcha — never
never is the empty union. When a naked conditional meets never, the distribution produces no branches — and the result is never regardless of the true/false branches. This is usually what you want (it filters cleanly) but occasionally surprises:
type Boxed<T> = T extends any ? { value: T } : never;
type B1 = Boxed<string>; // { value: string }
type B2 = Boxed<never>; // never — not { value: never }!
// To force a single instantiation, wrap:
type BoxedStrict<T> = [T] extends [any] ? { value: T } : never;
type B3 = BoxedStrict<never>; // { value: never }
This is also why Exclude<T, never> returns T unchanged — distributing over never leaves the original type alone.
infer in conditional types
infer declares a type variable that TypeScript fills in when the conditional type is matched:
// Extract the element type of an array
type ElementType<T> = T extends (infer E)[] ? E : never;
type M = ElementType<string[]>; // string
type N = ElementType<Array<User>>; // User
type O = ElementType<boolean>; // never
// Extract the resolved type of a Promise
type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T; // recursive
type P = Awaited<Promise<string>>; // string
type Q = Awaited<Promise<Promise<number>>>; // number
// Extract function return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function greet(name: string): string { return `Hello, ${name}`; }
type R = ReturnType<typeof greet>; // string
// Extract first argument
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type S = FirstArg<(x: number, y: string) => void>; // number
// Extract Promise value or leave plain value alone
type MaybeAwaited<T> = T extends Promise<infer U> ? U : T;
Multiple infer positions:
// Extract key and value types from a Map
type MapTypes<T> = T extends Map<infer K, infer V> ? { key: K; value: V } : never;
type T = MapTypes<Map<string, number>>; // { key: string; value: number }
Template literal types
Template literal types are string types built with backtick syntax:
type Greeting = `Hello, ${string}`;
const g: Greeting = "Hello, world"; // OK
const h: Greeting = "Hi there"; // Error — doesn't match pattern
// Union distribution in template literals
type Colors = "red" | "green" | "blue";
type ColorVariant = `${Colors}-light` | `${Colors}-dark`;
// "red-light" | "red-dark" | "green-light" | "green-dark" | ...
// CSS custom property names
type CSSVar = `--${string}`;
const v: CSSVar = "--primary-color"; // OK
// Event handler names from an event map
type EventMap = { click: MouseEvent; keydown: KeyboardEvent; focus: FocusEvent };
type HandlerName = `on${Capitalize<keyof EventMap & string>}`;
// "onClick" | "onKeydown" | "onFocus"
type Handlers = {
[K in keyof EventMap as `on${Capitalize<string & K>}`]: (e: EventMap[K]) => void;
};
// { onClick: (e: MouseEvent) => void; onKeydown: ...; onFocus: ... }
Combining mapped + conditional types
DeepPartial<T>
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface AppConfig {
server: { host: string; port: number };
database: { url: string; pool: { min: number; max: number } };
}
type PartialConfig = DeepPartial<AppConfig>;
// All nested properties become optional
DeepReadonly<T>
type DeepReadonly<T> = T extends (infer U)[]
? ReadonlyArray<DeepReadonly<U>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
type FrozenConfig = DeepReadonly<AppConfig>;
FlattenObject<T> (one level)
type FlattenObject<T extends Record<string, unknown>> = {
[K in keyof T]: T[K] extends Record<string, unknown>
? { [SK in keyof T[K]]: T[K][SK] }
: T[K];
}[keyof T];
// More useful: dot-notation keys
type DotKeys<T, Prefix extends string = ""> = {
[K in keyof T & string]: T[K] extends Record<string, unknown>
? DotKeys<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`;
}[keyof T & string];
type ConfigKeys = DotKeys<AppConfig>;
// "server.host" | "server.port" | "database.url" | "database.pool.min" | "database.pool.max"
Conditional property based on another property
// If T['kind'] is "circle", require radius; if "square", require side
type ShapeProps<T extends { kind: string }> = T["kind"] extends "circle"
? { kind: "circle"; radius: number }
: T["kind"] extends "square"
? { kind: "square"; side: number }
: never;
Pick based on value type
type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
interface Model {
id: number;
name: string;
active: boolean;
createdAt: Date;
updatedAt: Date;
}
type StringFields = PickByValue<Model, string>; // { name: string }
type DateFields = PickByValue<Model, Date>; // { createdAt: Date; updatedAt: Date }
type BoolFields = PickByValue<Model, boolean>; // { active: boolean }
Recursive types and depth limits
Recursive conditional types (TS 4.1+) and recursive mapped types are how deep transformations like DeepPartial, DeepReadonly, and dot-key derivations are expressed. The trick is that a type can reference itself in either branch of a conditional or in the value position of a mapped type. The compiler enforces a hard recursion limit — historically ~50 instantiations, raised to ~1000 with tail-call detection in newer TS releases — so deeply nested input or naïve walking can hit "Type instantiation is excessively deep and possibly infinite."
Tail-recursive accumulator pattern
Like at runtime, the cure for stack-style recursion is an accumulator. When TypeScript spots that the recursive call is the result of the conditional (not nested inside another type constructor), it can optimise the recursion away.
// Naive — depth-limited
type RepeatNaive<S extends string, N extends number> =
N extends 0 ? "" : `${S}${RepeatNaive<S, /* … */ Prev<N>>}`;
// Tail-recursive — fast and deep
type RepeatTail<S extends string, N extends number, Acc extends string = ""> =
N extends 0 ? Acc : RepeatTail<S, Prev<N>, `${Acc}${S}`>;
type Prev<N extends number> = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10][N];
type R = RepeatTail<"ab", 4>; // "abababab"
Depth-limited walks
When walking an unbounded shape, cap the depth explicitly. This is the standard guard for production-grade DeepKeys/Get/Set utilities.
type DotKeys<T, Depth extends number = 4, P extends string = ""> =
[Depth] extends [0] ? never :
{
[K in keyof T & string]: T[K] extends Record<string, unknown>
? `${P}${K}` | DotKeys<T[K], Prev<Depth>, `${P}${K}.`>
: `${P}${K}`;
}[keyof T & string];
Empirically, depth 4–6 covers the great majority of real config trees while keeping the compiler responsive.
Verifying recursion safety
The fastest way to know you're inside the budget is to compile and time:
npx tsc --noEmit --extendedDiagnostics
Output:
Files: 5
Lines of Library: 37000
Lines of Definitions: 4200
Lines of TypeScript: 120
...
Instantiations: 32100
Memory used: 95MB
Total time: 0.34s
If Instantiations jumps by orders of magnitude when you add a new recursive type, that type is the suspect.
Cross-references
The deep mechanics of infer are catalogued in infer-keyword; template literal pattern matching is covered in detail in template-literal-types; see utility-types for the standard-library utilities all of these patterns power.
Mapped type quick-reference
// Add readonly to all
type Readonly<T> = { readonly [K in keyof T]: T[K] }
// Remove readonly from all
type Mutable<T> = { -readonly [K in keyof T]: T[K] }
// Make all optional
type Partial<T> = { [K in keyof T]?: T[K] }
// Make all required
type Required<T> = { [K in keyof T]-?: T[K] }
// Transform value types
type Nullify<T> = { [K in keyof T]: T[K] | null }
// Rename keys
type Prefixed<T> = { [K in keyof T & string as `my_${K}`]: T[K] }
// Filter keys by value type (keep only functions)
type Methods<T> = { [K in keyof T as T[K] extends Function ? K : never]: T[K] }
Common pitfalls
- Forgetting
& stringon key remapping —keyof Tincludesstring | number | symbol. Template literal renames like\get${Capitalize}`requireK extends string. Intersect withstring:[K in keyof T & string as ...]`. - Distribution surprises with
never— Naked conditionals collapse toneverwhen givennever. If you want a guaranteed single instantiation, wrap with[T] extends [...]. - Filtering with
neverkeeps optional markers — Remapping a key toneverdrops it, but the optional?marker can survive in some compiler versions. Re-apply via a downstreamRequired/Partialpass if uniformity matters. - Recursive types that aren't tail-recursive —
\${X}${Repeat<...>}`` blows past the limit on long inputs; the same logic restructured to thread an accumulator stays comfortable. Always prefer accumulator-style for repeat/split/replace. - Mapped type over an indexed-access result —
{ [K in keyof T[U]]: ... }works, but error messages are awkward. ExtractT[U]to a named alias first. asclause that produces a non-string key — Remapping to a number or symbol changes the resulting object's key type. Be explicit if downstream code expects string keys only.- Conditional inside a mapped value can break inference —
{ [K in keyof T]: T[K] extends Fn ? Wrap<T[K]> : T[K] }distributes ifT[K]is a union. Wrap with[T[K]]when you want the union as a whole. - Template-literal mapped types eat
numberandbigint— When a key is numeric,\${K}`coerces silently. If you need to preserve numericness, branch onK extends number ? ... : ...`. Pick<T, K>whereKincludes invalid keys —Pickis permissive; an invalidKreturns{}. Prefertype-fest'sExcept/Pickfamily for strict checking, or constrain withK extends keyof Tin your own utilities.Required<Partial<T>>is notT—Requiredstrips?, but it doesn't restore the original optional/required pattern ofT. If you want round-trip identity, store the original and reuse it.
Real-world recipes
Recipe 1 — Form-state derivation
Derive a form-state type from a value type: every field becomes { value, error, touched }, all built from a single source-of-truth interface.
interface Form {
email: string;
age: number;
agree: boolean;
}
type FormState<T> = {
[K in keyof T]: {
value: T[K] | "";
error: string | null;
touched: boolean;
};
};
type FormUi = FormState<Form>;
// {
// email: { value: string | ""; error: string | null; touched: boolean };
// age: { value: number | ""; error: string | null; touched: boolean };
// agree: { value: boolean | ""; error: string | null; touched: boolean };
// }
const initial: FormUi = {
email: { value: "", error: null, touched: false },
age: { value: "", error: null, touched: false },
agree: { value: "", error: null, touched: false },
};
console.log(initial.email);
Output:
{ value: '', error: null, touched: false }
Recipe 2 — Action-creator type from a discriminated union
Generate one constructor per variant of a discriminated union — a common Redux pattern that benefits hugely from key remapping.
type Action =
| { type: "increment"; by: number }
| { type: "decrement"; by: number }
| { type: "reset" };
type ActionCreators = {
[A in Action as A["type"]]: (payload: Omit<A, "type">) => A;
};
const actions: ActionCreators = {
increment: (p) => ({ type: "increment", ...p }),
decrement: (p) => ({ type: "decrement", ...p }),
reset: (p) => ({ type: "reset", ...p }),
};
console.log(actions.increment({ by: 5 }));
Output:
{ type: 'increment', by: 5 }
Recipe 3 — Synthesize getter/setter pairs
Map an object to its accessor interface — one getX and one setX(value) per field.
type Accessors<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
} & {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
interface User { id: number; name: string }
type UserAccess = Accessors<User>;
// {
// getId: () => number; getName: () => string;
// setId: (v: number) => void; setName: (v: string) => void;
// }
Recipe 4 — Filter an object to writable, plain-data fields
Pair distribution with a value-type check to drop methods, leaving only data fields.
type DataOnly<T> = {
[K in keyof T as T[K] extends Function ? never : K]: T[K];
};
class User {
id = 1;
name = "Alice";
greet() { return `Hi, ${this.name}`; }
}
type UserData = DataOnly<User>;
// { id: number; name: string }
Recipe 5 — DeepMutable for editing a frozen config
Pair -readonly with recursion through arrays and objects for a real DeepMutable.
type DeepMutable<T> =
T extends ReadonlyArray<infer U>
? Array<DeepMutable<U>>
: T extends object
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
: T;
interface Frozen {
readonly meta: { readonly name: string; readonly tags: readonly string[] };
}
type Editable = DeepMutable<Frozen>;
// { meta: { name: string; tags: string[] } }
const e: Editable = { meta: { name: "x", tags: [] } };
e.meta.name = "y";
e.meta.tags.push("a");
console.log(e);
Output:
{ meta: { name: 'y', tags: [ 'a' ] } }
Recipe 6 — Validators map from a schema type
Generate a validators interface where each key carries the field's value type into its callback.
type Validators<T> = {
[K in keyof T]?: (value: T[K]) => string | null;
};
interface SignupForm { email: string; password: string; age: number }
const validators: Validators<SignupForm> = {
email: (v) => v.includes("@") ? null : "invalid",
password: (v) => v.length >= 8 ? null : "too short",
age: (v) => v >= 18 ? null : "must be 18+",
};
console.log(validators.email!("alice@example.com"));
console.log(validators.age!(16));
Output:
null
must be 18+
Recipe 7 — Pretty-print a tooltip with Simplify
Intersections show up as A & B & C in tooltips. A Simplify mapped type flattens them — purely cosmetic but invaluable for library authors.
type Simplify<T> = { [K in keyof T]: T[K] } & {};
type Messy = { a: string } & { b: number } & { c: boolean };
type Clean = Simplify<Messy>;
// Hover shows: { a: string; b: number; c: boolean }
The trailing & {} is the trick that nudges TypeScript to compute the flattened shape. type-fest's Simplify and Simplify-Deep use the same idea.
Recipe 8 — XOR of property keys
Encode "exactly one of these keys, never both" using a distributive mapped type plus never.
type Without<T, U> = { [K in Exclude<keyof T, keyof U>]?: never };
type XOR<T, U> = (Without<T, U> & U) | (Without<U, T> & T);
type ImageOrText = XOR<{ src: string; alt: string }, { text: string }>;
const a: ImageOrText = { src: "/x.png", alt: "x" }; // OK
const b: ImageOrText = { text: "hello" }; // OK
// const c: ImageOrText = { src: "/x.png", alt: "x", text: "hi" }; // Error
console.log(a, b);
Output:
{ src: '/x.png', alt: 'x' } { text: 'hello' }