Conditional Types in TypeScript
Conditional types let you write type-level if/else logic that picks different output types based on the input. Learn the syntax, how to constrain generics, and practical patterns.
TypeScript conditional types are types that pick between two other types based on a condition. They work at the type level the same way a ternary expression works at the value level. Instead of asking "is this value truthy?", a conditional type asks "is this type assignable to that type?"
The syntax mirrors JavaScript's ternary:
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"When you pass a string, the condition string extends string is true and TypeScript returns "yes". When you pass a number, the condition is false and the result is "no".
By itself this is not very useful. The power comes from using conditional types with generics, where the input type is not known ahead of time and the output type should change based on what comes in.
Replacing function overloads with a conditional type
Imagine a createLabel function that returns an IdLabel for numbers and a NameLabel for strings. Without conditional types, you would write overloads:
interface IdLabel {
id: number;
}
interface NameLabel {
name: string;
}
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "unimplemented";
}Three overloads for two input types. Add a third input type and you need six overloads. The number grows fast.
A conditional type replaces all of that with a single generic function:
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
const a = createLabel("typescript");
// a is NameLabel
const b = createLabel(42);
// b is IdLabel
const c = createLabel(Math.random() > 0.5 ? "hello" : 42);
// c is NameLabel | IdLabelWhen the argument is a string, the condition is false and the return type is NameLabel. When the argument is a number, the condition is true and you get IdLabel.
When the argument is the union string | number, the conditional type distributes over each member and produces NameLabel or IdLabel. The next section covers this union behavior in more detail.
Using conditional types as constraints
A conditional type can also act as a constraint. The true branch gives TypeScript extra information about the type being checked.
Here the true branch reads a message property off T. Without the constraint, TypeScript would reject this because a generic type parameter has no known properties:
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
type EmailContents = MessageOf<Email>;Inside the true branch, TypeScript knows that T has a message property, so indexing with T["message"] is valid. In the false branch, never signals that the type has no message.
The same pattern works for extracting array element types. If the input is an array, return the element type. Otherwise, return the type unchanged:
type Flatten<T> = T extends any[] ? T[number] : T;
type Str = Flatten<string[]>;
// string
type Num = Flatten<number>;
// numberUsing infer to capture type parts
The infer keyword captures a piece of the matched type so you can use it in the true branch. It is the natural partner of conditional types. For a full guide, see Infer Keyword in TypeScript.
Here is a type that extracts the element type of an array using infer:
type ElementOf<T> = T extends Array<infer Item> ? Item : never;
type E = ElementOf<string[]>;
// stringinfer Item declares a type variable in the element position. When the pattern matches, TypeScript fills in Item with whatever the element type actually is.
You can also add constraints to the inferred type using infer Item extends SomeType, which was added in TypeScript 4.7. This lets the true branch run only when the inferred type satisfies the constraint.
Distributing conditional types over unions
When a conditional type receives a union, it distributes over each member of the union individually instead of checking the union as a whole:
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// ^? string[] | number[]TypeScript applies ToArray<string> and ToArray<number> separately, then unions the results. This is usually what you want, but you can disable it by wrapping both sides of extends in square brackets:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result = ToArrayNonDist<string | number>;
// ^? (string | number)[]Now the union is checked as a single type and the result is one array type with a union element. For a deeper explanation, see Distributive Conditional Types in TypeScript.
Real-world pattern: extracting promise values
A common use of conditional types is unwrapping promise values. The built-in Awaited type uses this pattern:
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
type P1 = Awaited<Promise<string>>;
// ^? string
type P2 = Awaited<Promise<Promise<number>>>;
// ^? numberThe type recursively unwraps promises until it reaches a non-promise value. This is a recursive conditional type. For more on that pattern, see Recursive Types in TypeScript.
Common mistakes with conditional types
One common mistake is expecting a conditional type to narrow inside a function body the way a runtime type guard does. They do not, since conditional types are evaluated at the type level based on the type parameter, not on runtime control flow.
Use typeof checks and if statements to narrow values at runtime. Use conditional types to narrow return types at the type level.
Another mistake is forgetting that never in a distributive conditional type produces never as the result. Since never is the empty union, there are no members to distribute over and the conditional does not run. Wrap in [T] if you need to detect never specifically.
Rune AI
Key Insights
- Conditional types pick between two types using the
T extends U ? X : Ysyntax. - They replace function overloads when the return type depends on the input type.
- The true branch gives TypeScript extra information about the checked type.
- Built-in types like
ReturnType,Exclude, andAwaitedare all conditional types. - Distributive behavior over unions is the default and can be disabled with
[T].
Frequently Asked Questions
What is a conditional type in TypeScript?
When should I use conditional types instead of function overloads?
Conclusion
Conditional types give you type-level decision-making. They replace repetitive overloads, let you extract and transform types based on structure, and form the foundation for more advanced patterns like infer and distributive conditional types. Start with a simple T extends U ? X : Y and build up from there.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.