string | number into a conditional type, TypeScript does not evaluate the union as a single entity. It evaluates string and number individually, and then combines the resulting types back into a new union. This is called Distribution.1. What is Distribution?#
When a conditional type acts on a generic type parameter (like T), and that parameter is a naked union type (meaning it isn’t wrapped in an array, a promise, or an object), the conditional type distributes.
Let’s look at a type that wraps its input in an Array:
// The Conditional Type
type ToArray<T> = T extends any ? T[] : never;
// Let's pass a union of two primitives:
type MixedArray = ToArray<string | number>;The “Logical” Expectation vs the Compiler’s Reality#
If you evaluate this intuitively, you might expect the result to be (string | number)[] (an array where each element can be either a string or a number).
But because T is a naked generic parameter, TypeScript distributes.
Instead of doing this:
(string | number) extends any ? (string | number)[] : never
TypeScript evaluates it like this:
(string extends any ? string[] : never) | (number extends any ? number[] : never)
Which ultimately resolves to:
string[] | number[]
(An array of ONLY strings, OR an array of ONLY numbers!)
2. Rebuilding Utility Types: Exclude<T, U>#
This distribution mechanism is exactly how the built-in Exclude<T, U> utility type works under the hood.
Let’s rebuild it ourselves to trace the compiler’s execution steps:
// If T is assignable to U, return never (delete it).
// Otherwise, return T (keep it).
type CustomExclude<T, U> = T extends U ? never : T;
type AvailableColors = "red" | "green" | "blue";
// 🟢 We want to exclude "red" from the union
type CoolColors = CustomExclude<AvailableColors, "red">;The Distribution Steps:#
When TypeScript sees CustomExclude<"red" | "green" | "blue", "red">, it iterates over the union T:
- Iteration 1:
("red" extends "red" ? never : "red")➡️ Evaluates tonever - Iteration 2:
("green" extends "red" ? never : "green")➡️ Evaluates to"green" - Iteration 3:
("blue" extends "red" ? never : "blue")➡️ Evaluates to"blue"
TypeScript then recombines the results into a new union:
Final Result: never | "green" | "blue"
Why does never disappear?#
In TypeScript’s set theory, never represents the empty set.
If you combine an empty set with other sets using a Union, the empty set simply vanishes. It acts exactly like adding zero in mathematics!
never | "green" | "blue" collapses perfectly down to "green" | "blue".
3. Why does TypeScript do this?#
Distributive conditional types exist because they map perfectly to how functions behave with union types at runtime.
If you have a function that accepts string | number, you are technically saying “this function accepts a string, OR it accepts a number”. You are not passing a mystical “hybrid” string-number object.
Therefore, type-level transformations should apply to the individual underlying variants of the union, rather than attempting to transform the union as a monolithic concept.
Summary & Next Steps#
In this episode:
- We defined Distributive Conditional Types.
- We saw how
ToArray<string | number>evaluates tostring[] | number[]. - We traced the exact compiler steps used by the
Excludeutility type. - We learned that
neveracts as the “Zero” of union types and vanishes when recombined.
But what happens when you don’t want a union to distribute? What if you genuinely want to evaluate (string | number) as a single monolithic block?
In Episode 43: Preventing Distribution, we will learn how to turn this behavior off!

