TypeScript Conditional Types
Understanding Conditional Types in TypeScript
Conditional types in TypeScript enable you to create types that depend on other types, similar to how if-else statements work in JavaScript.
They're a powerful feature that allows for sophisticated type transformations and type-level programming.
Key Concepts
- Type-level logic: Perform conditional checks on types
- Type inference: Extract and manipulate types using
infer
- Composition: Combine with other TypeScript features
- Utility types: Build powerful type utilities
Common Use Cases
- Type-safe function overloading
- API response type transformations
- Complex type validations
- Building reusable type utilities
- Advanced type inference
Basic Conditional Type Syntax
Conditional types use the form T extends U ? X : Y
, which means:
"if type T
extends (or is assignable to) type U
, use type X
, otherwise use type Y
".
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'
Try it Yourself »
Conditional Types with Unions
Distributive Conditional Types
Conditional types are particularly useful with union types, where they're automatically distributed over union members:
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"
Try it Yourself »
Type Inference with infer
Extracting Types from Complex Structures
The infer
keyword allows you to declare a type variable within the condition part of a conditional type and then use it in the true branch of the condition:
// 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
Try it Yourself »
Built-in Conditional Types
Standard Library Utilities
TypeScript includes several built-in conditional types in its standard library:
// 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
Try it Yourself »
Advanced Patterns and Techniques
Recursive Conditional Types
Conditional types can be used recursively to create complex type transformations:
// 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
Try it Yourself »
Type-Level If-Else Chains
Chain multiple conditions together for complex type logic:
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"
Try it Yourself »
Conditional types are powerful when creating generic utilities and type-safe libraries:
// 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)
Try it Yourself »
Best Practices
Do:
- Use conditional types for complex type transformations
- Combine with
infer
for type extraction - Create reusable type utilities
- Document complex conditional types
- Test edge cases in your type definitions
Don't:
- Overuse complex conditional types when simple types would suffice
- Create deeply nested conditional types that are hard to understand
- Forget about performance implications with very complex types
- Use conditional types for runtime logic
Performance Considerations
- Deeply nested conditional types can increase compile times
- Consider using type aliases for intermediate results
- Be mindful of TypeScript's recursion depth limits