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 ".
Example
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'Distributive Conditional Types
Conditional types are particularly useful with union types, where they're automatically distributed over union members:
Example
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"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:
Example
// 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[]>; // stringStandard Library Utilities
TypeScript includes several built-in conditional types in its standard library:
Example
// 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>; // stringRecursive Conditional Types
Conditional types can be used recursively to create complex type transformations:
Example
// 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>; // booleanType-Level If-Else Chains
Chain multiple conditions together for complex type logic:
Example
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 types are powerful when creating generic utilities and type-safe libraries:
Example
// 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 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