Conditional Types
Branch at the type level with conditional expressions, infer, and distributive behavior.
You are an expert in Conditional Types for writing type-safe TypeScript. ## Key Points - Wrap `T` in a tuple (`[T] extends [U]`) when you need to prevent distributive behavior over unions. - Chain conditional types for multi-step transformations rather than nesting deeply in a single expression. - Use `infer` to extract types from function signatures, promise wrappers, and generic containers instead of requiring callers to pass inner types explicitly. - Unexpected distribution: a conditional type with a naked type parameter distributes over unions, which can produce surprising results when `never` (the empty union) is passed in. - Excessive recursion depth in deeply recursive conditional types; TypeScript has a recursion limit (typically around 50 levels). ## Quick Example ```typescript type IsString<T> = T extends string ? true : false; type A = IsString<"hello">; // true type B = IsString<42>; // false ``` ```typescript type NonNullable<T> = T extends null | undefined ? never : T; type C = NonNullable<string | null | undefined>; // string ```
skilldb get typescript-patterns-skills/Conditional TypesFull skill: 119 linesConditional Types — TypeScript Patterns
You are an expert in Conditional Types for writing type-safe TypeScript.
Overview
Conditional types follow the syntax T extends U ? X : Y, choosing between two type branches based on an assignability check. Combined with the infer keyword, they can extract and transform types from complex structures. Use conditional types to build utility types, unwrap containers, and enforce relationships between input and output types.
Core Philosophy
Conditional types bring branching logic to the type level, letting you express "if this type matches that shape, then produce X, otherwise produce Y." This is the foundation of TypeScript's ability to compute types from other types, making the type system a small functional programming language that runs at compile time.
The real power of conditional types emerges with infer. Where basic conditional types choose between branches, infer lets you reach inside a type and extract its components — the return type of a function, the element type of an array, the resolved value of a promise. This means you can write generic utilities that adapt to any input shape without requiring callers to manually specify inner types.
Understanding distributive behavior is essential for using conditional types correctly. When a conditional type receives a union, it distributes the check over each member independently and collects the results back into a union. This is usually what you want — filtering, mapping, and transforming union members — but when it is not, wrapping the type parameter in a tuple disables distribution. Mastering when to distribute and when to prevent it is the key to writing conditional types that behave predictably.
Core Concepts
Basic conditional type:
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
Distributive behavior over unions:
type NonNullable<T> = T extends null | undefined ? never : T;
type C = NonNullable<string | null | undefined>; // string
When T is a bare type parameter and receives a union, the conditional distributes over each member independently.
The infer keyword — extracting inner types:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type ElementType<T> = T extends (infer E)[] ? E : T;
Implementation Patterns
Deep readonly:
type DeepReadonly<T> = T extends Function
? T
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
Filtering union members:
type ExtractFunctions<T> = T extends (...args: any[]) => any ? T : never;
type Mixed = string | (() => void) | number | ((x: number) => string);
type Fns = ExtractFunctions<Mixed>;
// (() => void) | ((x: number) => string)
Flattening nested promises:
type FlattenPromise<T> = T extends Promise<infer U>
? FlattenPromise<U>
: T;
type Deep = Promise<Promise<Promise<string>>>;
type Flat = FlattenPromise<Deep>; // string
Preventing distribution with tuple wrapping:
type IsUnion<T> = [T] extends [infer U]
? U extends U
? [T] extends [U]
? false
: true
: never
: never;
Best Practices
- Wrap
Tin a tuple ([T] extends [U]) when you need to prevent distributive behavior over unions. - Chain conditional types for multi-step transformations rather than nesting deeply in a single expression.
- Use
inferto extract types from function signatures, promise wrappers, and generic containers instead of requiring callers to pass inner types explicitly.
Common Pitfalls
- Unexpected distribution: a conditional type with a naked type parameter distributes over unions, which can produce surprising results when
never(the empty union) is passed in. - Excessive recursion depth in deeply recursive conditional types; TypeScript has a recursion limit (typically around 50 levels).
Anti-Patterns
Using conditional types where a simple generic constraint suffices. If you only need to restrict input types, T extends SomeShape on the generic parameter is clearer than a conditional type that maps non-matching types to never. Reserve conditional types for when you need to transform or branch the output type.
Deeply nesting conditional branches in a single expression. A five-level ternary at the type level is as unreadable as a five-level ternary at the value level. Break complex conditional logic into named intermediate types that each handle one decision.
Ignoring the never input edge case. When never (the empty union) is passed to a distributive conditional type, the result is always never because there are no union members to distribute over. This surprises callers who expect a meaningful fallback; guard against it if your utility type might receive never.
Relying on infer in contravariant positions without understanding the implications. When infer appears in a function parameter position, multiple candidates produce an intersection rather than a union. This is correct behavior but catches developers off guard when they expect union extraction.
Writing recursive conditional types without a termination case. Every recursive conditional type needs a base case that stops the recursion. Without one, the compiler hits its depth limit and produces an opaque error. Always ensure the non-recursive branch is reachable for every valid input.
Install this skill directly: skilldb add typescript-patterns-skills
Related Skills
Branded Types
Create nominally distinct types from structural primitives using phantom brands.
Builder Pattern
Implement fluent, type-accumulating builders that enforce required fields at compile time.
Discriminated Unions
Model mutually exclusive states with tagged union types and exhaustive narrowing.
Generic Constraints
Constrain generic type parameters to enforce structural and behavioral contracts at compile time.
Module Augmentation
Extend third-party and global type declarations without modifying source using module augmentation.
Template Literal Types
Construct and parse string-level types using template literal syntax for compile-time string validation.