Capítulo 9 de 36

Chapter 9: Conditional Types

Core Idea

T extends U ? TrueType : FalseType is an if-statement at the type level — mostly uninteresting on concrete types, but combined with generics it lets one type alias replace what would otherwise require an exponentially growing set of function overloads.

Key Concepts

  • Syntax and evaluation: when the type left of extends is assignable to the type on the right, the conditional resolves to the true branch; otherwise the false branch.
  • The real payoff is with generics: instead of writing N overloads for a function whose return type depends on which input type was passed, encode the decision once as type NameOrId<T extends number | string> = T extends number ? IdLabel : NameLabel, then write a single generic function createLabel<T extends number | string>(x: T): NameOrId<T>.
  • Conditional type constraints narrow within the true branch: type MessageOf<T> = T extends { message: unknown } ? T["message"] : never — inside the T extends {...} branch, TypeScript knows T has that shape and lets you index into it; types that don't match fall through to the false branch (never here, a sensible default for "doesn't apply").
  • infer: introduces a new type variable inside the extends clause of a conditional type instead of manually destructuring with indexed access — type Flatten<T> = T extends Array<infer Item> ? Item : T extracts an array's element type declaratively. The same pattern extracts a function's return type: type GetReturnType<T> = T extends (...args: never[]) => infer R ? R : never.
  • infer on an overloaded function type resolves against the last signature (presumed to be the most general/catch-all one) — TypeScript can't do overload resolution based on a hypothetical argument list here.
  • Distributive conditional types: a conditional type built directly from a bare generic type parameter (Type extends any ? Type[] : never) automatically distributes over a union passed as that parameter — ToArray<string | number> becomes ToArray<string> | ToArray<number> = string[] | number[], not (string | number)[].
  • Suppressing distribution: wrap both sides of extends in a tuple ([Type] extends [any] ? ... : ...) — this stops the union from being distributed member-by-member, so ToArrayNonDist<string | number> yields (string | number)[] as a single non-distributed result.

Code Examples

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
type Str = Flatten<string[]>; // string
type Num = Flatten<number>;   // number (unchanged — not an array)
  • What it demonstrates: infer declaratively pulling an array's element type instead of a manual T[number] indexed-access lookup.
type ToArray<Type> = Type extends any ? Type[] : never;
type Distributed = ToArray<string | number>;    // string[] | number[]
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
type NotDistributed = ToArrayNonDist<string | number>; // (string | number)[]
  • What it demonstrates: the same conditional shape with and without the tuple-wrapping trick that suppresses union distribution.

Reference Tables

PatternEffect
T extends U ? A : Btype-level if/else on assignability
T extends { message: unknown } ? T["message"] : neverconstrain-and-extract in one step; unmatched types fall to a safe default
T extends Array<infer Item> ? Item : Tdeclarative extraction via infer, instead of manual indexed access
Type extends any ? ... : ... (bare param)distributes over unions
[Type] extends [any] ? ... : ...suppresses distribution

Key Takeaways

  1. Reach for a conditional type instead of a growing pile of function overloads any time the return type is a deterministic function of one input type.
  2. infer is the idiomatic way to pull a piece out of a matched type inside a conditional — prefer it over manual indexed-access gymnastics when the shape you're extracting from is generic/unknown.
  3. Distribution over unions is usually what you want; when it isn't (you want the union treated as one type), wrap both sides of extends in [...].

Connects To

  • Generics: conditional types are almost always paired with a generic type parameter.
  • Indexed Access Types: the manual technique infer frequently replaces.
  • Mapped Types / Template Literal Types: the remaining two Type Manipulation tools, often composed with conditional types for advanced utility types.
  • Utility Types (Reference): ReturnType<T>, Exclude<T, U>, and similar built-ins are implemented as conditional types with infer.