TypeScript Conditional Types Cheat Sheet
Covers the extends ? : syntax, infer keyword, distributive conditional types, and common utility type patterns built from them.
Basic Conditional Type Syntax
Conditional types select between two types based on an `extends` check, resolved at compile time.
type IsString<T> = T extends string ? true : false;type A = IsString<"hi">; // truetype B = IsString<42>; // falsetype Flatten<T> = T extends Array<infer Item> ? Item : T;type C = Flatten<string[]>; // stringtype D = Flatten<number>; // number (falls through)
Extracting Types with `infer`
`infer` introduces a type variable that TypeScript solves for within the conditional branch.
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;type Fn = () => Promise<string>;type R1 = ReturnOf<Fn>; // Promise<string>type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;type R2 = UnwrapPromise<Promise<number>>; // numbertype FirstArg<T> = T extends (arg: infer A, ...rest: any[]) => any ? A : never;type R3 = FirstArg<(id: string, name: string) => void>; // string
Distributive Conditional Types
Conditional types distribute over naked union types — a key gotcha and a key feature.
type ToArray<T> = T extends any ? T[] : never;type Result = ToArray<string | number>;// distributes: ToArray<string> | ToArray<number> => string[] | number[]// Wrap in a tuple to disable distribution:type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;type Result2 = ToArrayNonDist<string | number>;// (string | number)[] — the union stays intact
Built-in Utility Types Using This Pattern
Standard library utilities that are just conditional types under the hood.
type Exclude<T, U> = T extends U ? never : T;type Extract<T, U> = T extends U ? T : never;type NonNullable<T> = T extends null | undefined ? never : T;type Status = "idle" | "loading" | "error" | "success";type Errored = Extract<Status, "error" | "success">; // "error" | "success"type NotErrored = Exclude<Status, "error">; // "idle" | "loading" | "success"
Common Conditional Type Patterns
Recipes you'll reuse across codebases.
- T extends U ? X : Y- core ternary form, evaluated per union member if T is naked
- infer- captures a subpart of T for use in the true branch
- [T] extends [U]- wraps in tuple to prevent distribution over unions
- T extends any ? T : never- forces distribution explicitly
- Awaited<T>- built-in recursive conditional type that unwraps nested Promises
- Parameters<T> / ReturnType<T>- infer-based extraction of function signatures
Recursive Conditional Types (DeepPartial / DeepReadonly)
Conditional types can reference themselves to walk arbitrarily nested object structures.
type DeepPartial<T> = T extends Function ? T : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;type DeepReadonlyArr<T> = T extends (infer U)[] ? ReadonlyArray<DeepReadonlyArr<U>> : T extends object ? { readonly [K in keyof T]: DeepReadonlyArr<T[K]> } : T;interface Config { host: string; retries: { max: number; delayMs: number } }type PartialConfig = DeepPartial<Config>;// { host?: string; retries?: { max?: number; delayMs?: number } }
Tail-Recursive Conditional Types (TS 4.5+)
The compiler evaluates tail-recursive conditional types near-iteratively, avoiding the usual instantiation-depth limit.
// Tail-recursive: the recursive call IS the result, not wrapped in// another type (no `[T, ...Recurse<...>]`).type TrimLeft<S extends string> = S extends ` ${infer Rest}` ? TrimLeft<Rest> : S;type Repeat<T, N extends number, Acc extends unknown[] = []> = Acc['length'] extends N ? Acc : Repeat<T, N, [...Acc, T]>;type Five = Repeat<'x', 5>; // ['x', 'x', 'x', 'x', 'x']// Non-tail-recursive version hits "Type instantiation is excessively// deep and possibly infinite" far sooner because each call wraps the// next instead of replacing it:type RepeatSlow<T, N extends number, Acc extends unknown[] = []> = Acc['length'] extends N ? Acc : [T, ...RepeatSlow<T, N, Acc>];
UnionToIntersection via Contravariant `infer`
Distributing a union into a function parameter position and inferring back flips the union into an intersection.
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;type Parts = { a: 1 } | { b: 2 } | { c: 3 };type Combined = UnionToIntersection<Parts>;// { a: 1 } & { b: 2 } & { c: 3 }// Why it works: function PARAMETER types are contravariant, so when// the union distributes across `(k: U) => void`, TS must find a type// assignable to every branch's parameter — that's the intersection.
`infer` Position Determines Union vs. Intersection
The same infer variable used in multiple covariant positions unifies to a union; in contravariant positions it unifies to an intersection.
// Covariant positions (return-like) -> union of candidatestype MergeReturns<T> = T extends { a: infer U; b: infer U } ? U : never;type M = MergeReturns<{ a: string; b: number }>; // string | number// Contravariant positions (function parameters) -> intersectiontype MergeParams<T> = T extends { f(x: infer P): void; g(x: infer P): void } ? P : never;type P1 = MergeParams<{ f(x: { a: 1 }): void; g(x: { b: 2 }): void }>;// { a: 1 } & { b: 2 }
Conditional Type Gotchas
Sharp edges that trip up even experienced TypeScript users.
- Distributing over `never`- `T extends any ? X : Y` with `T = never` produces `never` (an empty union short-circuits before X/Y ever apply)
- Deferred resolution- inside a generic function body, `T extends U ? X : Y` stays unresolved until T is instantiated with a concrete type
- Excessively deep errors- non-tail-recursive conditional recursion hits TypeScript's instantiation-depth ceiling (roughly 50 frames) much sooner than a tail-recursive accumulator form
- Boolean literal distribution- a generic `T extends true ? A : B` distributes separately over `true` and `false` when T is an unresolved boolean type parameter
- `extends unknown` as a distribution trigger- a common idiom to force distribution, but it silently distributes even when you only meant a plain type check
- `infer` only inside `extends`- you cannot introduce an `infer` type variable in the true/false branches, only within the `extends` clause itself
When a conditional type doesn't distribute the way you expect, check whether T is a 'naked' type parameter — distribution only happens when T appears alone on the left of extends, not wrapped in a tuple, array, or object.