Conditional Types
45 minConditional types are one of TypeScript's most powerful features, enabling you to create types that depend on other types through conditional logic. They use the `extends` keyword to check if one type extends another, then select one type or another based on the result. Conditional types enable sophisticated type transformations and are the foundation of many advanced TypeScript patterns and utility types.
The basic syntax of a conditional type is `T extends U ? X : Y`, which reads as "if T extends U, then X, else Y". This ternary-like syntax allows you to express type relationships that depend on other types. Conditional types are evaluated at compile time and enable the type system to make decisions based on type relationships, creating dynamic type behavior.
The `infer` keyword within conditional types allows you to extract and name types from other types. For example, you can extract the return type of a function, the element type of an array, or the resolved type of a promise. `infer` is used in the "true" branch of a conditional type and creates a type variable that can be used in the resulting type. This is how utility types like `ReturnType<T>` and `Parameters<T>` are implemented.
Distributive conditional types are a special behavior where conditional types distribute over union types. When you write `T extends U ? X : Y` and T is a union type, the conditional type is applied to each member of the union separately, then the results are combined into a union. This behavior is powerful but can be surprising, and you can disable it by wrapping T in square brackets: `[T] extends [U] ? X : Y`.
Conditional types are used extensively in TypeScript's standard library for utility types like `Exclude<T, U>`, `Extract<T, U>`, `NonNullable<T>`, `ReturnType<T>`, and `Parameters<T>`. Understanding how these work internally helps you create your own utility types and understand advanced type patterns. They're also essential for creating type-safe APIs that adapt to different input types.
Common patterns with conditional types include type guards (checking if a type matches a pattern), type extraction (pulling types from complex structures), type transformation (modifying types based on conditions), and type narrowing (creating more specific types from general ones). Mastering conditional types enables you to create sophisticated, flexible type systems that adapt to your needs.
Key Concepts
- Conditional types use extends to create type-dependent logic.
- The syntax T extends U ? X : Y selects types based on conditions.
- The infer keyword extracts types from other types.
- Distributive conditional types apply to each union member separately.
- Conditional types enable dynamic type behavior at compile time.
Learning Objectives
Master
- Creating conditional types with extends keyword
- Using infer keyword to extract types from other types
- Understanding distributive conditional types
- Building utility types using conditional types
Develop
- Advanced type system thinking and type transformations
- Understanding type relationships and dependencies
- Designing flexible and adaptive type systems
Tips
- Use conditional types when types need to adapt based on other types.
- Use infer to extract types from function signatures, arrays, and promises.
- Understand distributive behavior when working with union types.
- Study built-in utility types to learn conditional type patterns.
Common Pitfalls
- Creating overly complex conditional types that are hard to understand.
- Not understanding distributive behavior, leading to unexpected results.
- Using conditional types when simpler type definitions would work.
- Forgetting that conditional types are evaluated at compile time, not runtime.
Summary
- Conditional types create types that depend on other types.
- They use extends keyword and ternary-like syntax.
- The infer keyword extracts types from complex type structures.
- Distributive conditional types apply to union members separately.
- They enable sophisticated type transformations and utility types.
Exercise
Create conditional types that transform types based on conditions.
type NonNullable<T> = T extends null | undefined ? never : T;
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
type ArrayElement<T> = T extends Array<infer U> ? U : never;
// Usage
type StringOrNull = string | null;
type NonNullString = NonNullable<StringOrNull>; // string
type FunctionReturn = ReturnType<() => number>; // number
type ArrayType = ArrayElement<string[]>; // string
const nonNull: NonNullString = "hello"; // Only string is allowed
Exercise Tips
- Use infer keyword to extract types: type Parameters<T> = T extends (...args: infer P) => any ? P : never;
- Create distributive conditional types: type ToArray<T> = T extends any ? T[] : never;
- Use conditional types with mapped types for complex transformations.
- Combine multiple conditions: type IsString<T> = T extends string ? true : false;