In TypeScript, given `type ToArray<T> = T extends unknown ? T[] : never`, why does `ToArray<string | number>` evaluate to `string[] | number[]` rather than `(string | number)[]`?
answer
- think multiplication over addition
- checked side must be bare T
- each member evaluated separately
- results unioned back together
- T[] or {x:T} blocks it
basics
~20 sA conditional type whose checked type is a bare type parameter distributes: TypeScript applies it to each union member separately and unions the results. ToArray runs once on string and once on number, producing string[] | number[].
solid answer
~50 sA conditional type is *distributive* when the type being checked is a naked type parameter — just `T`, not `T[]` or `{ x: T }`. Instantiate such a type with a union and TypeScript does not test the union as a whole; it substitutes each member for `T`, evaluates the conditional once per member, and unions the results. So `ToArray<string | number>` becomes `ToArray<string> | ToArray<number>`, which is `string[] | number[]`. If I wanted `(string | number)[]` I would stop distribution by wrapping both sides in one-element tuples — `[T] extends [unknown] ? T[] : never` — which tests the union as a single type. This mechanic is also what makes union filtering possible: return `T` in one branch and `never` in the other and the unwanted members disappear, because `never` contributes nothing to a union.
code
typescript · 8 linestype ToArray<T> = T extends unknown ? T[] : never;
type Distributed = ToArray<string | number>; // string[] | number[]
type ToArrayWhole<T> = [T] extends [unknown] ? T[] : never;
type NotDistributed = ToArrayWhole<string | number>; // (string | number)[]
const a: Distributed = ['x'];
const b: NotDistributed = ['x', 1];go deeper
Know that a conditional type looks like T extends U ? X : Y and that handing it a union can produce a union back rather than one answer. Being able to read the result is enough at this stage.
Be ready to state the precondition out loud — a naked type parameter instantiated with a union — and to walk the substitution member by member. Show the tuple wrapping that switches the behaviour off.
Expect to explain why a shared helper silently changed a consumer's type, using distribution as the diagnosis, and to decide deliberately whether a given helper should map over members or judge the union as a whole.
Own the API consequence: whether an exported generic distributes is observable behaviour that consumers depend on, so treat flipping it as a breaking change and pin it with type-level tests rather than leaving it implicit.
## The rule A conditional type has the shape `T extends U ? X : Y`. TypeScript gives it one extra behaviour that surprises almost everyone the first time: when the type being *checked* is a **naked type parameter** and the type argument supplied for it is a **union**, the conditional is not evaluated once against the whole union. It is evaluated once per union member, and the results are combined back into a union. That is called *distribution*, by analogy with distributing multiplication over addition: ```ts type ToArray<T> = T extends unknown ? T[] : never; type A = ToArray<string | number>; // evaluated as ToArray<string> | ToArray<number> // => string[] | number[] ``` The condition `T extends unknown` is deliberately trivially true here — the conditional exists only to trigger distribution, not to make a decision. `T extends any` is used interchangeably for the same purpose. ## What "naked" means Naked means the checked position contains the type parameter and nothing else. These distribute: ```ts type One<T> = T extends string ? 1 : 0; // naked T type Two<T> = T extends { id: string } ? 1 : 0; // naked T, structural condition ``` These do **not**, because `T` is wrapped in something before the check: ```ts type Three<T> = T[] extends string[] ? 1 : 0; // T[] is not naked type Four<T> = { x: T } extends { x: string } ? 1 : 0; type Five<T> = [T] extends [string] ? 1 : 0; // the standard opt-out ``` Only the checked (left) side matters. What appears after `extends`, and what the branches produce, has no effect on whether distribution happens. There is a second precondition that is easy to miss: distribution is a property of *instantiating a type parameter*. A union written literally in the checked position does not distribute, because no substitution takes place: ```ts type Direct = string | number extends string ? 1 : 0; // => 0, one single check ``` ## Why the compiler does this Distribution is what turns conditional types into a mapping operation over unions, which is where most of their practical value lives. Two idioms fall out of it directly. **Mapping.** Transform every member and keep the union shape — that is exactly the `ToArray` example, and the same pattern wraps each member in a `Promise`, a box type, or a nullable. **Filtering.** Return `T` in one branch and `never` in the other. Because `never` is the empty union, a member that resolves to `never` simply vanishes when the results are unioned: ```ts type OnlyStrings<T> = T extends string ? T : never; type B = OnlyStrings<string | 42 | boolean>; // => string ``` Notice `boolean` disappeared entirely. `boolean` is internally the union `true | false`, so it splits into two members, and neither is a string. This filtering idiom is the machinery underneath the built-in union-filtering utilities in `lib.es5.d.ts`. ## Turning it off The canonical opt-out is to wrap **both** sides in a one-element tuple: ```ts type IsExactlyString<T> = [T] extends [string] ? true : false; type C = IsExactlyString<string | number>; // => false ``` Now the checked type is `[T]`, a tuple containing the parameter rather than the bare parameter, so no substitution-per-member happens; the compiler asks a single assignability question about the whole union. Wrapping only one side does not work — the two sides must be comparable, so both get the tuple. ## Two edge inputs worth remembering `never` is the empty union. Distributing over zero members yields zero results, so a distributive conditional given `never` returns `never` without evaluating either branch. And instantiating a distributive conditional with `any` produces the union of *both* branches, because `any` is treated as matching and not matching simultaneously: ```ts type YesNo<T> = T extends string ? 'yes' : 'no'; type D = YesNo<any>; // => 'yes' | 'no' ``` ## It costs nothing at runtime All of this is compile-time arithmetic over types. TypeScript erases types on emit, so a distributive conditional produces no JavaScript at all — no checks, no branches, no allocation. The cost is compiler time and reader comprehension, never program speed. ## What an interviewer is listening for The strong answer names the precondition (naked type parameter plus a union argument), states the substitute-and-union mechanic, and shows the tuple opt-out. The weak answer treats distribution as something conditional types always do, and is then unable to explain why `[T] extends [U]` behaves differently or why a filter with `never` removes members instead of inserting `never` into the result.
- Does `type Direct = string | number extends string ? 1 : 0` distribute?No. Distribution happens when a type *parameter* in the checked position is instantiated with a union. Here the union is written literally, so there is no substitution — the compiler asks one question, is `string | number` assignable to `string`, gets no, and returns `0`.
- What comes out of a distributive conditional when you instantiate it with `any`?The union of both branches. For `type YesNo<T> = T extends string ? 'yes' : 'no'`, `YesNo<any>` is `'yes' | 'no'`. `any` is deliberately treated as both matching and not matching the condition, which is a useful smell test: if a helper suddenly returns a union of both outcomes, an `any` leaked into it.
- How do you use distribution to remove members from a union?Return `T` in one branch and `never` in the other: `T extends string ? T : never`. Each member is tested on its own, and the members that resolve to `never` disappear when the results are unioned, because `never` is the empty union and contributes nothing.
saying these in an interview costs you the question
- Says every conditional type tests the whole union at once
- Expects (string | number)[] and calls the union result a bug
- Thinks distribution happens even when T is wrapped, like T[]
- Claims distribution adds a runtime check to the emitted JavaScript
- Confuses distribution with mapped-type iteration over keys