TypeScript Conditional Types
A conditional type is a type that depends on another type. It works like an if-else statement, but for types instead of values.
Understanding Conditional Types in TypeScript
Conditional types let you build types that change depending on the type you give them. They make advanced type transformations and type-level programming possible.
Key concepts:
- Type-level logic: run a condition on a type
- Type inference: pull types apart with
infer - Composition: combine with other TypeScript features
- Utility types: build reusable type helpers
Common use cases:
- Type-safe function overloading
- Transforming API response types
- Complex type validation
- Reusable type utilities
- Advanced type inference
Basic Conditional Type Syntax
A conditional type has the form T extends U ? X : Y. Read it as: if type T extends (is assignable to) type U, use type X, otherwise use type Y.
Example
IsString<T> becomes true for string and for the literal "hello", because a literal extends its base type. For number it becomes false. Variables can use the resulting type too.
T extends U ? X : Y conditional type syntax.
Declare IsString<T> as T extends string ? true : false, then aliases Result1, Result2 and Result3 for string, number and the literal "hello" with comments giving true, false, true, and two variables a: IsString<string> and b: IsString<number> with comments noting their types 'true' and 'false'. Types only, nothing is printed.
Output: TypeScript file and captured output
type IsString<T> = T extends string ? true : false;
// Usage examples
type Result1 = IsString<string>; // true
type Result2 = IsString<number>; // false
type Result3 = IsString<"hello">; // true (literal types extend their base types)
// We can use this with variables too
let a: IsString<string>; // a has type 'true'
let b: IsString<number>; // b has type 'false'
Conditional Types with Unions
Conditional types are especially useful with union types. When the checked type is a union, the condition is applied to each member separately. This is called a distributive conditional type.
Distributive Conditional Types
ToArray<string | number> is split into ToArray<string> | ToArray<number>, which gives string[] | number[]. The same distribution lets ExtractString keep only the string members of a union by mapping the others to never.
ToArray<T> as T extends any ? T[] : never and StringOrNumberArray = ToArray<string | number> with comments explaining it becomes string[] | number[], then ExtractString<T> that keeps only string members and StringsOnly applied to string | number | boolean | "hello" with a comment giving string | "hello". Types only, nothing is printed.
Output: TypeScript file and captured output
type ToArray<T> = T extends any ? T[] : never;
// When used with a union type, it applies to each member of the union
type StringOrNumberArray = ToArray<string | number>;
// This becomes ToArray<string> | ToArray<number>
// Which becomes string[] | number[]
// We can also extract specific types from a union
type ExtractString<T> = T extends string ? T : never;
type StringsOnly = ExtractString<string | number | boolean | "hello">;
// Result: string | "hello"
Type Inference with infer
The infer keyword declares a type variable inside the condition of a conditional type. If the condition matches, the variable holds the captured type and you can use it in the true branch.
Extracting Types from Complex Structures
ReturnType captures the return type R of a function type. ElementType captures the element type U of an array type. With typeof greet you can feed a real function's type into these helpers.
infer keyword capturing a return type and an array element type.
Declare ReturnType<T> as T extends (...args: any[]) => infer R ? R : never, two functions greet() returning "Hello, world!" and getNumber() returning 42, and aliases GreetReturnType/NumberReturnType using typeof with comments giving string and number. Then declare ElementType<T> that infers U from (infer U)[] and aliases for number[] and string[]. Types only, nothing is printed; make the file a module so the local ReturnType does not clash with the global one.
Output: TypeScript file and captured output
// Extract the return type of a function type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Examples
function greet() { return "Hello, world!"; }
function getNumber() { return 42; }
type GreetReturnType = ReturnType<typeof greet>; // string
type NumberReturnType = ReturnType<typeof getNumber>; // number
// Extract element type from array
type ElementType<T> = T extends (infer U)[] ? U : never;
type NumberArrayElement = ElementType<number[]>; // number
type StringArrayElement = ElementType<string[]>; // string
// Make this file a module so the local ReturnType does not clash with the built-in global one
export {};
Built-in Conditional Types
TypeScript's standard library already includes several conditional types, so you do not need to write the common ones yourself.
Standard Library Utilities
Extract keeps the members of a union that match, Exclude removes them, NonNullable drops null and undefined, and Parameters and ReturnType take a function type apart.
OnlyStrings = Extract<string | number | boolean, string>, NoStrings = Exclude<string | number | boolean, string>, NotNull = NonNullable<string | null | undefined>, Params = Parameters<(a: string, b: number) => void> and Return = ReturnType<() => string>, each with a comment explaining the helper and the resulting type. Types only, nothing is printed.
Output: TypeScript file and captured output
// Extract<T, U> - Extracts types from T that are assignable to U
type OnlyStrings = Extract<string | number | boolean, string>; // string
// Exclude<T, U> - Excludes types from T that are assignable to U
type NoStrings = Exclude<string | number | boolean, string>; // number | boolean
// NonNullable<T> - Removes null and undefined from T
type NotNull = NonNullable<string | null | undefined>; // string
// Parameters<T> - Extracts parameter types from a function type
type Params = Parameters<(a: string, b: number) => void>; // [string, number]
// ReturnType<T> - Extracts the return type from a function type
type Return = ReturnType<() => string>; // string
Advanced Patterns and Techniques
Conditional types can call themselves, be chained like an if-else ladder, and describe the return type of a generic function.
Recursive Conditional Types
A conditional type can refer to itself. UnwrapPromise keeps unwrapping until the type is no longer a Promise, so even a promise inside a promise resolves to the plain inner type.
Promise types.
Declare UnwrapPromise<T> as T extends Promise<infer U> ? UnwrapPromise<U> : T, then aliases A, B and C for Promise<string>, Promise<Promise<number>> and boolean with comments giving string, number and boolean. Types only, nothing is printed.
Output: TypeScript file and captured output
// Deeply unwrap Promise types
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;
// Examples
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<Promise<Promise<number>>>; // number
type C = UnwrapPromise<boolean>; // boolean
Type-Level If-Else Chains
Several conditions can be chained, one after the other. TypeName checks a type against string, number, boolean, undefined and Function in turn, and falls back to "object".
TypeName<T> that maps string, number, boolean, undefined and Function to the string literals "string", "number", "boolean", "undefined" and "function", falling back to "object". Add aliases T0 to T4 for string, 42, true, () => void and Date[] with comments showing the results. Types only, nothing is printed.
Output: TypeScript file and captured output
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
// Usage
type T0 = TypeName<string>; // "string"
type T1 = TypeName<42>; // "number"
type T2 = TypeName<true>; // "boolean"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<Date[]>; // "object"
Conditional Return Types
Conditional types shine in generic utilities and type-safe libraries. Here a generic function's return type depends on the type of its argument. Inside the function, as any is needed because TypeScript cannot check the body against a conditional return type.
processValue<T>(value: T) returning string for strings, number for numbers, boolean for booleans and never otherwise; upper-case strings, double numbers, negate booleans (each with an as any assertion) and throw Unsupported type for anything else. Call it with "hello", 10 and true, storing stringResult, numberResult and boolResult with comments noting "HELLO", 20 and false. Nothing is printed.
Output: TypeScript file and captured output
// A function that returns different types based on input type
function processValue<T>(value: T): T extends string
? string
: T extends number
? number
: T extends boolean
? boolean
: never {
if (typeof value === "string") {
return value.toUpperCase() as any; // Type assertion needed due to limitations
} else if (typeof value === "number") {
return (value * 2) as any;
} else if (typeof value === "boolean") {
return (!value) as any;
} else {
throw new Error("Unsupported type");
}
}
// Usage
const stringResult = processValue("hello"); // Returns "HELLO" (type is string)
const numberResult = processValue(10); // Returns 20 (type is number)
const boolResult = processValue(true); // Returns false (type is boolean)
Best Practices
Do:
- Use conditional types for complex type transformations
- Combine them with
inferto extract parts of a type - Create reusable type utilities
- Document complex conditional types
- Test the edge cases of your type definitions
Don't:
- Reach for complex conditional types when a simple type is enough
- Build deeply nested conditional types that are hard to read
- Forget that very complex types slow down the compiler
- Use conditional types for runtime logic — they exist only at compile time
Performance considerations:
- Deeply nested conditional types increase compile times
- Store intermediate results in type aliases
- Keep TypeScript's recursion depth limits in mind