Introduction
TypeScript’s type system is Turing-complete, and one of the key features that unlocks its full power is conditional types. Just like ternary operators in JavaScript (condition ? a : b), conditional types let you create type-level logic: T extends U ? X : Y. This enables dynamic, context-aware type resolution.
Basic Syntax
A conditional type checks whether a type extends another type:
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
The condition T extends U asks: “Is T assignable to U?” If yes, resolve to the true branch; otherwise, the false branch.
The infer Keyword
The infer keyword allows you to extract a type from within a condition:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function greet() { return 'hello'; }
type G = ReturnType<typeof greet>; // string
infer R declares a type variable R that is inferred from the matched position. This is the foundation of many utility types.
Distributive Conditional Types
When a conditional type is used with a union, it distributes over each member:
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[] (not (string | number)[])
This distribution happens automatically unless you wrap both sides:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// (string | number)[]
Built-in Utility Types Using Conditionals
Many standard utility types rely on conditional types:
// Exclude — removes types from a union
type T0 = Exclude<'a' | 'b' | 'c', 'a'>;
// 'b' | 'c'
// Extract — keeps only matching types
type T1 = Extract<string | number | (() => void), Function>;
// () => void
// NonNullable — removes null and undefined
type T2 = NonNullable<string | number | null | undefined>;
// string | number
Here’s how Exclude is implemented:
type MyExclude<T, U> = T extends U ? never : T;
Real-World Patterns
Conditional Return Types Based on Input
type AsyncResult<T> = T extends Promise<infer U> ? U : T;
async function fetchData(): Promise<string> {
return 'data';
}
type Data = AsyncResult<ReturnType<typeof fetchData>>; // string
Function Overloads Without Overloads
type StringOrNumber<T> = T extends string ? string : number;
function process<T extends string | number>(val: T): StringOrNumber<T>;
function process(val: any): any {
return typeof val === 'string' ? val.toUpperCase() : val * 10;
}
const a = process('hello'); // string
const b = process(42); // number
Deep Conditional Type Example
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
interface User {
name: string;
address: { city: string; zip: number };
}
type ReadonlyUser = DeepReadonly<User>;
// { readonly name: string; readonly address: { readonly city: string; readonly zip: number } }
Conclusion
Conditional types are the if/else of the type system. Combined with infer, distribution, and mapped types, they enable sophisticated type transformations that catch bugs at compile time. Master conditionals to write truly type-safe, self-documenting TypeScript code.
