TypeScript Distributive Conditional Types: The Union Distribution Rule
T extends U ? X : Y behaves completely differently when T is a union—TypeScript distributes the conditional over each member. Master this rule to unlock advanced utility types.
Write T extends U ? X : Y where T is a union, and TypeScript
doesn't evaluate it once—it distributes the conditional over each union member separately,
then unions the results. This behavior powers many utility types, but it catches developers
off guard when they don't expect it.
The Distribution Rule
When a conditional type's checked type (T in T extends U ? X : Y)
is a naked type parameter that receives a union type,
the conditional distributes over each member:
type ToArray<T> = T extends unknown ? T[] : never;
// What you might expect:
type Result = ToArray<string | number>;
// Result = (string | number)[] ❌ WRONG
// What actually happens:
// 1. Distribute: ToArray<string> | ToArray<number>
// 2. Evaluate each: string[] | number[]
type Result = ToArray<string | number>;
// Result = string[] | number[] ✅ ACTUALWhen Distribution Happens
Distribution requires all three conditions:
- Conditional type: Must be
T extends U ? X : Yform - Naked type parameter:
Tmust appear alone, not wrapped - Union type argument: The type passed to
Tmust be a union
// ✅ Distributes: T is naked
type A<T> = T extends string ? 'yes' : 'no';
type R1 = A<string | number>; // 'yes' | 'no'
// ❌ Does NOT distribute: T is wrapped in array
type B<T> = T[] extends string[] ? 'yes' : 'no';
type R2 = B<string | number>; // 'no' (union array ≠ string array)
// ❌ Does NOT distribute: T is wrapped in tuple
type C<T> = [T] extends [string] ? 'yes' : 'no';
type R3 = C<string | number>; // 'no' (evaluated as whole)Practical Examples
Built-in Utility Types
Several built-in types rely on distribution:
// Exclude: Remove types from union
type Exclude<T, U> = T extends U ? never : T;
type Result = Exclude<'a' | 'b' | 'c', 'a'>;
// Distributes:
// ('a' extends 'a' ? never : 'a') | ('b' extends 'a' ? never : 'b') | ...
// = never | 'b' | 'c'
// = 'b' | 'c'
// Extract: Keep only matching types
type Extract<T, U> = T extends U ? T : never;
type Result2 = Extract<string | number | boolean, string | number>;
// = string | number
// NonNullable: Remove null and undefined
type NonNullable<T> = T extends null | undefined ? never : T;
type Result3 = NonNullable<string | null | undefined>;
// = stringFiltering Object Keys
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Get keys where value extends a type
type KeysMatching<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
type StringKeys = KeysMatching<User, string>;
// = 'name' | 'email'
type NumberKeys = KeysMatching<User, number>;
// = 'id' | 'age'Preventing Distribution
Sometimes you want to evaluate the union as a whole, not distribute. Wrap the type parameter in a tuple or array:
// Distributes (naked T)
type IsString<T> = T extends string ? true : false;
type R1 = IsString<string | number>; // boolean (true | false)
// Does NOT distribute (T wrapped in tuple)
type IsStringOnly<T> = [T] extends [string] ? true : false;
type R2 = IsStringOnly<string | number>; // false
// Practical use: Check if type is exactly a union
type IsUnion<T, C = T> = T extends C
? [C] extends [T]
? false
: true
: never;
type R3 = IsUnion<string>; // false
type R4 = IsUnion<string | number>; // trueThe never Edge Case
never is the empty union—it has zero members. Distributing over zero
members produces never, not the false branch:
type ToArray<T> = T extends unknown ? T[] : 'fallback';
// never distributes over... nothing
type Result = ToArray<never>;
// = never (not 'fallback'!)
// Why? Distribution: never = union of 0 members
// ToArray<never> = union of 0 results = never
To handle never specially, check for it first with the wrapped pattern:
type ToArraySafe<T> = [T] extends [never]
? never // or some fallback
: T extends unknown
? T[]
: never;
type R1 = ToArraySafe<string | number>; // string[] | number[]
type R2 = ToArraySafe<never>; // never (explicit, not accidental)Distribution with infer
Distribution also affects infer in conditional types:
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
// Distributes over union of Promises
type Result = UnwrapPromise<Promise<string> | Promise<number>>;
// = string | number
// Mixed union: some match, some don't
type Result2 = UnwrapPromise<Promise<string> | number>;
// = string | number (Promise unwrapped, number passed through)Extracting Function Parameters
type ParamTypes<T> = T extends (...args: infer P) => unknown ? P : never;
type Fn1 = (a: string) => void;
type Fn2 = (a: number, b: boolean) => void;
// Distribution over function union
type Params = ParamTypes<Fn1 | Fn2>;
// = [string] | [number, boolean]
// Useful for overload-like behavior handlingCommon Gotchas
1. Unexpected boolean
type IsString<T> = T extends string ? true : false;
// boolean is just true | false
type R = IsString<string | number>;
// = true | false
// = boolean <-- Simplified by TypeScript
// This often surprises developers expecting true or false2. Object Type Distribution
// Objects in unions distribute too
type Example<T> = T extends { a: number } ? T['a'] : never;
type Result = Example<{ a: 1 } | { a: 2 } | { b: 3 }>;
// Distributes:
// { a: 1 } extends { a: number } ? 1 : never → 1
// { a: 2 } extends { a: number } ? 2 : never → 2
// { b: 3 } extends { a: number } ? ... : never → never
// = 1 | 23. Mapped Types Don't Distribute
// This is NOT distribution—it's mapped type behavior
type Nullable<T> = { [K in keyof T]: T[K] | null };
// Unions work differently in mapped types
type Result = Nullable<{ a: string } | { b: number }>;
// = { a: string | null } | { b: number | null }
// (mapped types preserve union structure)Advanced: Distributive + Recursive
// Deep unwrap nested Promises
type DeepUnwrap<T> = T extends Promise<infer U>
? DeepUnwrap<U>
: T;
type R1 = DeepUnwrap<Promise<Promise<Promise<string>>>>;
// = string
type R2 = DeepUnwrap<Promise<string> | Promise<Promise<number>>>;
// Distributes, then recursively unwraps each
// = string | number
// Flatten nested arrays (1 level)
type Flatten<T> = T extends (infer U)[] ? U : T;
type R3 = Flatten<string[] | number[][]>;
// = string | number[]Quick Reference
| Pattern | Distributes? |
|-----------------------------------|--------------|
| T extends U ? X : Y | Yes |
| [T] extends [U] ? X : Y | No |
| T[] extends U[] ? X : Y | No |
| { p: T } extends { p: U } ? X : Y | No |
| T & U (intersection, not conditional) | N/A |
| Type Argument | Distribution Result |
|---------------|--------------------------|
| string | Single evaluation |
| string | number | Evaluates each member |
| never | Returns never |
| unknown | Single evaluation |
| any | Usually returns union |Key Takeaways
- Conditional types distribute over unions when the checked type is a naked type parameter
Exclude,Extract, andNonNullableall rely on this behavior- Wrap in
[T]to prevent distribution:[T] extends [U] ? X : Y neveris the empty union—distribution over it returnsnever- Use wrapped patterns to detect and handle
neverexplicitly - Distribution +
infer= powerful type extraction from union members
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Related Insights
Explore related edge cases and patterns
Advertisement