Articles

TypeScript Conditional Types Explained

Conditional types let TypeScript pick a type based on another type, using T extends U ? X : Y — the foundation of most advanced type utilities.

Takina Takina · · 4 min read
Code editor showing TypeScript source

Conditional types let TypeScript choose between two types based on a relationship check, written as T extends U ? X : Y. It reads like a ternary expression, but it operates entirely at the type level: T is checked for assignability to U, and the type resolves to X if that holds, or Y otherwise. They’re the mechanism behind most of TypeScript’s built-in utility types, and understanding them is what turns “I copy generic type snippets from Stack Overflow” into “I can write my own.”

The basic syntax

type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">; // true
type B = IsString<42>;       // false

T extends U here doesn’t mean class inheritance — it means “is T assignable to U,” the same relationship TypeScript already uses for structural type checking. Any type can appear on either side, including unions, primitives, and object shapes.

Distributive conditional types

When T is a naked type parameter — used directly, not wrapped in something like T[] or [T] — and you pass a union as T, the conditional type distributes over each member of the union individually:

type ToArray<T> = T extends unknown ? T[] : never;

type Result = ToArray<string | number>; // string[] | number[]

TypeScript evaluates ToArray<string> | ToArray<number> and unions the results, rather than treating string | number as one opaque type. This is usually what you want, but it can surprise you if you expected the union to be checked as a whole. Wrapping both sides in a tuple ([T] extends [U] ? X : Y) disables distribution when you need the non-distributive behavior instead.

Inferring types with infer

The infer keyword introduces a new type variable inside the extends clause, letting you extract a piece of a type rather than just testing it:

type ElementType<T> = T extends (infer U)[] ? U : never;

type Item = ElementType<string[]>; // string

type ReturnOf<T> = T extends (...args: never[]) => infer R ? R : never;

type Result = ReturnOf<() => number>; // number

This is precisely how TypeScript’s built-in ReturnType<T> and Parameters<T> are implemented — both are conditional types with an infer clause, not special compiler magic. If you’ve used the utility types covered in our guide to TypeScript’s utility types, you’ve already been using conditional types indirectly.

Chaining conditions

Conditional types can nest, which gives you a type-level equivalent of an if / else if / else chain:

type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

Each branch is checked in order, and the first match wins — the same short-circuit behavior as a runtime if chain, just resolved by the compiler instead of at runtime.

Practical example: a safer event map

A common real-world use is narrowing a return type based on an input key, which is how strongly-typed event emitters and API clients are usually built:

type EventMap = {
  click: { x: number; y: number };
  keydown: { key: string };
};

type Payload<K extends keyof EventMap> = K extends keyof EventMap
  ? EventMap[K]
  : never;

function handle<K extends keyof EventMap>(event: K, payload: Payload<K>) {
  // payload is typed exactly to the event name passed in
}

handle("click", { x: 10, y: 20 }); // OK
handle("click", { key: "Enter" }); // Type error

Without the conditional type, payload would have to be typed as the union of every possible event payload, and the caller would lose the connection between event and payload.

Conditional types vs function overloads

Both can express “the return type depends on the input,” but they solve it differently:

Conditional typesFunction overloads
Where it livesIn the type itself, reusable across functionsTied to one function signature
ComposabilityCombines with other generics and utility typesEach overload is a separate, fixed signature
Distributes over unionsYes, automaticallyNo — each overload matches independently
Readability at call siteSometimes harder to trace in error messagesUsually clearer error messages

Prefer overloads for a single function with a small, fixed set of input/output shapes. Prefer conditional types when the relationship needs to be reused across multiple generics, or when it needs to distribute over a union automatically.

Where this fits with satisfies

Conditional types narrow type definitions; the satisfies operator checks that a value matches a type without widening its inferred type. They’re complementary — you might use a conditional type to define a precise return type, then use satisfies at a call site to validate a literal against it without losing literal-type information.

If you’re newer to TypeScript’s type system generally, our TypeScript getting-started guide and generics explainer are good prerequisites before conditional types — they build directly on generic type parameters.

The takeaway

A conditional type is T extends U ? X : Y, evaluated entirely by the compiler. Naked type parameters distribute over unions automatically; infer lets you pull a piece out of a matched type instead of just testing it; and chained conditions behave like an if / else if ladder. Once these click, most of TypeScript’s utility-type library — ReturnType, Exclude, Extract, NonNullable — stops looking like magic and starts looking like ordinary conditional types you could have written yourself.

Takina Takina · · 4 min read

TypeScript Abstract Classes, Explained

Abstract classes in TypeScript define shared implementation plus methods subclasses must fill in. How they differ from interfaces and when to reach for them.

#TypeScript #JavaScript #Web Development
Takina Takina · · 3 min read

TypeScript's never Type, Explained

never represents values that can't exist — it marks unreachable code, exhaustive switches, and functions that always throw or loop forever.

#TypeScript #JavaScript #Web Development
Takina Takina · · 4 min read

TypeScript readonly Modifiers Explained

TypeScript's readonly keyword blocks reassignment at compile time for properties, arrays, and tuples — with no runtime enforcement at all.

#TypeScript #JavaScript #Web Development