Conditional Types
Conditional types let a type decide between two outcomes based on a relationship between types, using the syntax T extends U ? X : Y. They are the type system’s equivalent of a ternary expression: if T is assignable to U, the type resolves to X, otherwise it resolves to Y. This unlocks type-level branching that adapts to whatever type a generic receives, and it is the mechanism behind many of the standard utility types. This page covers the basic form, chaining conditions, using conditional types with generics, and how they interact with never.
Basic syntax
A conditional type tests whether one type extends (is assignable to) another and picks a branch accordingly. The check is resolved by the compiler, not at runtime — there is no JavaScript output.
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true (literals are assignable to string)
Here T extends string asks “is T assignable to string?” When T is string or a string literal the answer is true, otherwise false. The whole expression evaluates to a literal type that you can use anywhere a type is expected.
Branching on input types
The real power appears when the branches return different shapes. A common pattern is normalizing a flexible input type into a single canonical form.
type Flatten<T> = T extends readonly (infer Item)[] ? Item : T;
type Num = Flatten<number[]>; // number
type Str = Flatten<string>; // string (already not an array)
type Mix = Flatten<boolean[]>; // boolean
Flatten strips one level of array: if T is an array, it resolves to the element type; otherwise it leaves T untouched. This lets a single helper accept both T and T[] and always work with the element type.
Chaining conditions
Conditional types can be nested to express multiple cases, much like an if/else if/else ladder. Each false branch becomes the next test.
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type T1 = TypeName<string>; // "string"
type T2 = TypeName<() => void>; // "function"
type T3 = TypeName<{ x: 1 }>; // "object"
The conditions are evaluated top to bottom, and the first matching branch wins. Order matters — more specific checks should precede broader ones, just as in runtime branching.
Constraining generics with conditionals
Conditional types frequently appear in generic function signatures to make the return type depend on the argument. This produces precise inference without overloads.
function unwrap<T>(value: T): T extends Promise<infer R> ? R : T {
// implementation can use type assertions internally
return (value instanceof Promise ? value : value) as never;
}
declare const p: Promise<number>;
declare const n: number;
const a = unwrap(p); // number
const b = unwrap(n); // number
The return type T extends Promise<infer R> ? R : T resolves differently for each call site: a Promise<number> yields number, while a plain number stays number. The caller always gets the unwrapped value type.
Distributing over unions and never
When the checked type is a naked type parameter and the argument is a union, the conditional distributes over each member — covered in depth on the distributive conditional types page. A related subtlety is that never acts as the empty union, so a distributive conditional applied to never resolves to never.
type NonNullableLike<T> = T extends null | undefined ? never : T;
type Clean = NonNullableLike<string | null | undefined>; // string
type Empty = NonNullableLike<never>; // never
| Expression | Result | Why |
|---|---|---|
1 extends number ? "y" : "n" | "y" | 1 is assignable to number |
string extends number ? "y" : "n" | "n" | not assignable |
T extends never ? "y" : "n" (T = never) | never | distributes over empty union |
(() => 1) extends Function ? "y" : "n" | "y" | functions extend Function |
Wrap a parameter in a tuple —
[T] extends [U] ? ...— to switch off distribution when you want the union checked as a whole rather than member by member.
Best Practices
- Reach for conditional types when a type must change shape based on its input.
- Order chained conditions from most specific to most general, like runtime branches.
- Pair conditionals with
inferto extract types from the matched branch. - Use
[T] extends [U]to disable distribution when you need the union tested atomically. - Remember that a distributive conditional over
nevercollapses tonever. - Prefer a single conditional return type over multiple function overloads when feasible.
- Keep deeply nested conditionals readable by extracting intermediate named aliases.