A conditional type chooses between two types with a test that reads like the JavaScript ternary: T extends U ? X : Y. If T is assignable to U, the result is X; otherwise it is Y.
extends here means "is assignable to", the same relation the compiler uses when you assign a value to a variable. Conditional types exist only at compile time; they are erased from the JavaScript output.
The Syntax
type Result = CheckedType extends TestType ? TrueType : FalseType;
Conditional types become useful with generics, where the checked type is a type parameter that gets a concrete type later. In the true branch TypeScript knows the checked type matches the test, so T["message"] above is allowed even though plain T has no message property.
They also nest, like chained ternaries:
The as TypeName<T> in the function body is not optional, as the next section shows.
Conditional Return Types Need an Assertion
A function whose return type is a conditional type over its own type parameter cannot return either branch directly. TypeScript does not narrow T inside the body, so it cannot tell which branch applies:
index.ts(5,34): error TS2322: Type 'number' is not assignable to type 'Flip<T>'.
index.ts(5,45): error TS2322: Type 'string' is not assignable to type 'Flip<T>'.
Two common fixes: overloads, which state each input-output pair and check callers precisely, or an assertion in the implementation.
Overloads are covered on the function overloading page. With an assertion, the compiler trusts you: a wrong branch in the body would not be caught.
Distributive Conditional Types
When the checked type is a bare type parameter and it receives a union, the condition runs once per member and the results are joined into a new union:
Distribution is what makes Exclude and Extract work. Exclude<T, U> is defined as T extends U ? never : T: each member that matches U becomes never, and never disappears from a union. So Exclude<"a" | "b" | "c", "a"> is "b" | "c".
Two surprises come from the same rule. boolean is the union true | false, so ToArray<boolean> is false[] | true[], not boolean[]. And never is the empty union, so a distributive conditional type given never returns never without testing anything:
type IsNever<T> = T extends never ? true : false;
type X = IsNever<never>; // never, not true
type IsNeverFixed<T> = [T] extends [never] ? true : false;
type Y = IsNeverFixed<never>; // true
Extracting Types with infer
infer declares a new type variable inside the extends clause. If the match succeeds, TypeScript fills that variable in from the checked type, and you can use it in the true branch:
Read T extends Promise<infer V> ? V : T as "if T is a promise of something, call that something V and return it; otherwise return T unchanged". infer is allowed only in the extends clause of a conditional type.
An infer variable can carry its own constraint with extends. The match then succeeds only if the inferred type fits:
type FirstString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;
type A = FirstString<["a", 1]>; // "a"
type B = FirstString<[1, "a"]>; // never: the first element is not a string
Building ReturnType Yourself
The built-in ReturnType is a one-line conditional type with infer. Writing it yourself is the classic exercise that makes both ideas click:
typeof makeUser turns the function value into its type, then the conditional type matches it against "any function" and captures the return type as R. The standard library version differs in two details: its parameter is constrained to function types (T extends (...args: any) => any), so ReturnType<string> is a compile error rather than never, and its false branch is any. The ReturnType page covers Parameters, InstanceType and Awaited too.
Recursive Conditional Types
A conditional type can refer to itself, which lets it unwrap any depth of nesting:
type Flatten<T> = T extends readonly (infer U)[] ? Flatten<U> : T;
type A = Flatten<number[][][]>; // number
type B = Flatten<string>; // string
The built-in Awaited<T> works this way, unwrapping Promise<Promise<T>> down to T. Keep recursion shallow in practice: very deep or unbounded recursion makes the compiler give up with error TS2589: Type instantiation is excessively deep and possibly infinite.
Quick Reference
| Pattern | Meaning |
|---|---|
T extends U ? X : Y | X if T is assignable to U, else Y |
T extends U ? never : T | remove members matching U (this is Exclude) |
T extends U ? T : never | keep members matching U (this is Extract) |
[T] extends [U] ? X : Y | same test, without distributing over a union |
T extends (infer E)[] ? E : T | element type of an array |
T extends Promise<infer V> ? V : T | value type of a promise |
T extends (...args: any[]) => infer R ? R : never | return type of a function |
T extends [infer H, ...infer Rest] ? ... | first element and the rest of a tuple |
Frequently Asked Questions
What is a conditional type in TypeScript?
A type of the form T extends U ? X : Y. If T is assignable to U, the result is X, otherwise Y. It is an if/else for types, evaluated at compile time; nothing of it exists in the emitted JavaScript.
What does the infer keyword do in TypeScript?
infer declares a type variable inside the extends clause of a conditional type and lets TypeScript fill it in from the matched type. T extends Promise<infer V> ? V : T pulls the resolved value type out of a promise type. It can only be used in the extends clause of a conditional type.
What is a distributive conditional type?
When the checked type is a bare type parameter and you pass a union, the condition is applied to each member separately and the results are joined. ToArray<string | number> with type ToArray<T> = T extends unknown ? T[] : never becomes string[] | number[]. Wrap both sides in brackets, [T] extends [unknown], to turn this off.
How do I get the return type of a function in TypeScript?
Use the built-in ReturnType<typeof fn>. It is a conditional type with infer: T extends (...args: any) => infer R ? R : any. For async functions, wrap it in Awaited<...> to get the resolved value instead of the promise.
Why does IsNever<never> return never instead of true?
never is the empty union, and a distributive conditional type maps over the members of a union. With no members, there is nothing to map and the result is never. Write [T] extends [never] ? true : false to test for never itself.