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.
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 types | Function overloads | |
|---|---|---|
| Where it lives | In the type itself, reusable across functions | Tied to one function signature |
| Composability | Combines with other generics and utility types | Each overload is a separate, fixed signature |
| Distributes over unions | Yes, automatically | No — each overload matches independently |
| Readability at call site | Sometimes harder to trace in error messages | Usually 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.
Keep reading
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.
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.
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.