Conditional Types Distribute Only Under Specific Conditions
A working model for distributivity, never, infer, and the tuple-wrapper technique.
Conditional types let a type-level program choose one result when an input is assignable to another type and a different result otherwise.
type ElementType<T> = T extends readonly (infer Item)[] ? Item : T;
type A = ElementType<string[]>; // string
type B = ElementType<number>; // number
The syntax resembles a ternary expression, but unions add an important behaviour: some conditional types distribute over union members.
A naked type parameter is distributive
When the checked side of extends is a type parameter by itself, applying the conditional to a union evaluates each member separately and unions the results.
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[]
It is useful to imagine ToArray<A | B> becoming ToArray<A> | ToArray<B>. This is how helpers filter unions and transform variants while preserving their correlations.
All conditional types distribute over unions, including conditionals whose checked side is a concrete alias rather than a type parameter. The presence of a union anywhere to the left of extends is sufficient:
type Input = string | number;
type Concrete = Input extends string ? 'text' : 'other';
// 'text' | 'other'
This makes named aliases a convenient way to opt into member-by-member evaluation without introducing a generic parameter.
Tuple wrapping controls distribution
Sometimes the question concerns the union as a whole. Wrapping both sides in one-element tuples prevents a naked type parameter from appearing directly in the checked position.
type IsString<T> = T extends string ? true : false;
type IsEntirelyString<T> = [T] extends [string] ? true : false;
type Each = IsString<string | number>; // boolean
type Whole = IsEntirelyString<string | number>; // false
The tuple is not about runtime arrays; it changes the type relationship being tested. The same pattern is common when detecting never:
type IsNever<T> = [T] extends [never] ? true : false;
Using T extends never works just as well for T = never, producing true. Tuple wrapping is mostly stylistic in this case and is more relevant for ordinary unions.
never disappears during distribution
never behaves as the bottom type and is removed when combined with other members of a union. This makes it especially useful as the false branch of a union filter.
Filtering relies on that disappearance:
type KeepStrings<T> = T extends string ? T : never;
type Names = KeepStrings<'Ada' | 42 | 'Lin'>;
// 'Ada' | 'Lin'
The numeric member maps to never, which vanishes when unioned with the remaining strings.
infer names a matched part
Inside the true branch, infer introduces a type variable derived from the matched structure.
type AwaitedValue<T> = T extends PromiseLike<infer Value>
? AwaitedValue<Value>
: T;
This recursively unwraps promise-like layers. Because the outer conditional is distributive, a union of promise and non-promise inputs is handled member by member.
Inference can occur in multiple positions. Candidates gathered from covariant positions tend to form unions, while candidates from contravariant positions can form intersections. Complex helpers become easier to review when they first match one structural fact and then delegate another transformation to a named helper.
Conditional types are less surprising once two questions are explicit: is the checked type a naked type parameter, and should the operation apply to each union member or to the union as a whole?