TypeScript conditional types are one of the most powerful features for creating flexible and reusable type definitions. Whether you’re new to TypeScript or looking to level up your type system knowledge, this guide will help you master conditional types from the ground up.
Conditional types allow you to create types that depend on other types, similar to how if statements work in regular JavaScript. This powerful feature enables you to write more dynamic and reusable type definitions.
Table of Contents
- What Are Conditional Types?
- Basic Conditional Type Patterns
- Practical Use Cases
- Advanced Patterns
- Best Practices
- Common Gotchas and Solutions
- Practical Examples
What Are Conditional Types?
Conditional types take the form T extends U ? X : Y
, where:
- T is the type we’re checking
- U is the condition type
- X is the type if condition is true
- Y is the type if condition is false
Let’s look at a simple example:
type IsString<T> = T extends string ? true : false;
// Usage
type Result1 = IsString<'hello'>; // true
type Result2 = IsString<123>; // false
Code language: JavaScript (javascript)
Basic Conditional Type Patterns
Let’s explore some common patterns you’ll encounter when working with conditional types:
1. Type Checking
type IsArray<T> = T extends any[] ? true : false;
type StringArray = IsArray<string[]>; // true
type StringType = IsArray<string>; // false
Code language: JavaScript (javascript)
2. Type Extraction
type UnwrapArray<T> = T extends Array<infer U> ? U : T;
type ElementType = UnwrapArray<number[]>; // number
type NonArrayType = UnwrapArray<string>; // string
Code language: HTML, XML (xml)
Practical Use Cases
1. Function Return Type Inference
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function greet() { return 'Hello'; }
function add(a: number, b: number) { return a + b; }
type GreetReturn = GetReturnType<typeof greet>; // string
type AddReturn = GetReturnType<typeof add>; // number
Code language: JavaScript (javascript)
2. Nullable Type Handling
type NonNullable<T> = T extends null | undefined ? never : T;
type Example = NonNullable<string | null | undefined>; // string
Code language: JavaScript (javascript)
Advanced Patterns
1. Distributive Conditional Types
Conditional types are distributive when used with union types:
type ToArray<T> = T extends any ? T[] : never;
type StringOrNumber = string | number;
type ArraysType = ToArray<StringOrNumber>; // string[] | number[]
Code language: HTML, XML (xml)
2. Type Inference in Conditional Types
type FirstArgument<T> = T extends (first: infer U, ...args: any[]) => any ? U : never;
function example(name: string, age: number) {}
type FirstArg = FirstArgument<typeof example>; // string
Code language: JavaScript (javascript)
Best Practices
- Keep It Simple: Start with basic conditional types and gradually move to more complex patterns as needed.
// Good
type IsNumber<T> = T extends number ? true : false;
// Too Complex
type ComplexType<T> = T extends Array<infer U>
? U extends object
? U extends Function
? ReturnType<U>
: never
: never
: never;
Code language: PHP (php)
- Use Meaningful Type Names: Choose descriptive names that reflect the purpose of your conditional types.
// Good
type ExtractPromiseValue<T> = T extends Promise<infer U> ? U : T;
// Bad
type E<T> = T extends Promise<infer U> ? U : T;
Code language: HTML, XML (xml)
- Document Complex Types: Add comments to explain the purpose and behavior of complex conditional types.
// Extracts the type of array elements, returns 'never' for non-array types
type ElementOf<T> = T extends Array<infer E> ? E : never;
Code language: JavaScript (javascript)
Common Gotchas and Solutions
1. Handling Never Type
type HandleNever<T> = [T] extends [never] ? true : false;
type Test1 = HandleNever<never>; // true
type Test2 = HandleNever<string>; // false
Code language: JavaScript (javascript)
2. Working with Union Types
type NonDistributive<T> = [T] extends [string | number] ? true : false;
type Test3 = NonDistributive<string | number>; // true
type Test4 = NonDistributive<boolean>; // false
Code language: JavaScript (javascript)
Practical Examples
Building a Type-Safe Event System
type EventMap = {
click: { x: number; y: number };
change: { value: string };
};
type EventType<T extends keyof EventMap> = EventMap[T];
function handleEvent<T extends keyof EventMap>(event: T, data: EventType<T>) {
// Type-safe event handling
console.log(event, data);
}
// Usage
handleEvent('click', { x: 100, y: 200 }); // Valid
handleEvent('change', { value: 'new value' }); // Valid
// handleEvent('click', { value: 'error' }); // Type error!
Code language: JavaScript (javascript)
Conditional types are a cornerstone of TypeScript’s type system, enabling you to create flexible and reusable type definitions. As you continue working with TypeScript, you’ll find conditional types invaluable for building type-safe applications.
Practice with these patterns and examples to become more comfortable with conditional types. Remember to start simple and gradually work your way up to more complex use cases as your understanding grows.
For more TypeScript insights, check out our guide on TypeScript Union Types vs Intersections which complements this knowledge perfectly.