Expand every union nested inside objects and tuples into a union of union-free structures. The cartesian product is built one key and one element at a time.
A union buried three levels deep in an object still has to end up at the top.
DistributeUnions<T> takes a data structure made of objects and tuples on any level of nesting and returns the union of every union-free structure it could stand for. Where a plain distributive conditional only pulls apart a union sitting at the top level, this type has to reach into every element and every property and multiply the possibilities out.
type T1 = DistributeUnions<[1 | 2, 'a' | 'b']>
// => [1, 'a'] | [2, 'a'] | [1, 'b'] | [2, 'b']
type T2 = DistributeUnions<
{ type: 'a'; value: number | string } | { type: 'b'; value: boolean }
>
// => | { type: 'a'; value: number }
// | { type: 'a'; value: string }
// | { type: 'b'; value: false }
// | { type: 'b'; value: true }Hoisting the unions to the top also buys you something concrete: Exclude can then remove a single deep case, which it cannot do while the union sits nested. That is the whole of type ExcludeDeep<A, B> = Exclude<DistributeUnions<A>, B>.
Implement a type Distribute Unions, that turns a type of data structure containing union types into a union of
all possible types of permitted data structures that don't contain any union. The data structure can be any
combination of objects and tuples on any level of nesting.
For example:
type T1 = DistributeUnions<[1 | 2, 'a' | 'b']>
// => [1, 'a'] | [2, 'a'] | [1, 'b'] | [2, 'b']
type T2 = DistributeUnions<{ type: 'a', value: number | string } | { type: 'b', value: boolean }>
// => | { type 'a', value: number }
// | { type 'a', value: string }
// | { type 'b', value: boolean }
type T3 = DistributeUnions<[{ value: 'a' | 'b' }, { x: { y: 2 | 3 } }] | 17>
// => | [{ value: 'a' }, { x: { y: 2 } }]
// | [{ value: 'a' }, { x: { y: 3 } }]
// | [{ value: 'b' }, { x: { y: 2 } }]
// | [{ value: 'b' }, { x: { y: 3 } }]
// | 17For context, this type can be very useful if you want to exclude a case on deep data structures:
type ExcludeDeep<A, B> = Exclude<DistributeUnions<A>, B>
type T0 = ExcludeDeep<[{ value: 'a' | 'b' }, { x: { y: 2 | 3 } }] | 17, [{ value: 'a' }, any]>
// => | [{ value: 'b' }, { x: { y: 2 } }]
// | [{ value: 'b' }, { x: { y: 3 } }]
// | 17View on GitHub: https://tsch.js.org/869
Change the following code to make the test cases pass (no type check errors).
/* _____________ Your Code Here _____________ */
type DistributeUnions<T> = any
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'
type cases = [
// Already distributed unions should stay the same:
Expect<Equal<DistributeUnions<1>, 1>>,
Expect<Equal<DistributeUnions<string>, string>>,
Expect<Equal<DistributeUnions<1 | 2>, 1 | 2>>,
Expect<
Equal<
DistributeUnions<'b' | { type: 'a' } | [1]>,
'b' | { type: 'a' } | [1]
>
>,
// tuples:
Expect<Equal<DistributeUnions<[1 | 2, 3]>, [1, 3] | [2, 3]>>,
Expect<
Equal<
Unlock 150+ medium, hard, and extreme challenges to master advanced TypeScript.
Monthly subscription. Cancel anytime.
Five helper types feed a three-way switch:
type UnionToIntersection<U> = (
U extends unknown ? (arg: U) => void : never
) extends (arg: infer I) => void
? I
: never
type LastOfUnion<U> =
UnionToIntersection<U extends unknown ? () => U : never> extends () => infer L
? L
: never
type Flatten<T> = { [K in keyof T]: T[K] }
type DistributeObject<T, K = LastOfUnion<keyof T>> = [keyof T] extends [never]
? T
: K extends keyof T
? DistributeUnions<T[K]> extends infer V
? V extends unknown
? DistributeObject<Omit<T, K>> extends infer Rest
? Rest extends unknown
? Flatten<Rest & { [P in K]: V }>
: never
: never
: never
: never
: never
type DistributeTuple<
T extends readonly unknown[],
Acc extends unknown[] = [],
> = T extends readonly [infer Head, ...infer Tail]
? DistributeUnions<Head> extends infer H
? H extends unknown
? DistributeTuple<Tail, [...Acc, H]>
: never
: never
: Acc
type DistributeUnions<T> = T extends unknown
? T extends readonly unknown[]
? DistributeTuple<T>
: T extends object
? DistributeObject<T>
: T
: neverT extends unknown ? ... : never is a distributive conditional: because T is a naked type parameter, TypeScript splits the union and runs the branch once per member. 17 | [10, ...] enters the true branch twice, so the top level is already flat before any recursion starts. Each member is then classified: tuple, other object, or leaf. A leaf like string or 1 is returned unchanged, which is where the recursion bottoms out.
DistributeTuple peels the head off with [infer Head, ...infer Tail] and carries the result so far in Acc, a second type parameter with a default. That accumulator pattern is the type-level equivalent of a loop variable.
The two nested conditionals are doing different jobs. DistributeUnions<Head> extends infer H is only a binding: it names the distributed head so it is computed once. H extends unknown then distributes over it, and the recursion continues once per member with a different Acc each time.
// DistributeTuple<[1 | 2, 3]>
// H = 1 -> DistributeTuple<[3], [1]> -> [1, 3]
// H = 2 -> DistributeTuple<[3], [2]> -> [2, 3]
// Result: [1, 3] | [2, 3]Branching inside the recursion is what produces a product rather than a sum: every split multiplies the number of open branches, and each one runs to the empty tuple and returns its own Acc.
Tuples hand you a head. Objects do not, so LastOfUnion<keyof T> has to manufacture one. Wrapping each member of keyof T in () => U and intersecting the results with UnionToIntersection builds what is effectively an overloaded function; matching it against () => infer L makes TypeScript resolve infer from the last overload only, and out falls a single key.
[object Object]Which key comes out is a compiler detail and does not matter here, because the result is an unordered union either way. With one key in hand the rest mirrors the tuple case: distribute T[K], branch over the values, recurse on Omit<T, K>, and branch over whatever the recursive call returned.
The base case is [keyof T] extends [never]. The square brackets switch distribution off, which matters because keyof T of an emptied object is never, and never is the empty union: a bare keyof T extends never would distribute over zero members and evaluate to never instead of taking a branch.
Combining the recursive result with the key just peeled off is an intersection: Rest & { [P in K]: V }. Equal treats an intersection and the equivalent flat object as different types, so Flatten maps over the keys once to merge them. Being a homomorphic mapped type, it preserves optional and readonly modifiers while doing so.
// { x: 'a' } & { y: 'c' } is not Equal to { x: 'a'; y: 'c' }
// Flatten<{ x: 'a' } & { y: 'c' }> isDistributeUnions<string> and DistributeUnions<'b' | { type: 'a' } | [1]>, come back untouched.boolean is a union of false | true in the type system, so { type: 'b'; value: boolean } splits into two members even though no union was written by hand.{ kind: 'some'; value: 'x' | 'y' } sitting under an option property distributes there and then multiplies with the outer keys, because DistributeObject calls back into DistributeUnions for every property value.[false | true, { value: 'a' | 'b' }, { x: { y: 2 | 3 } }], give the full eight-member product.17 | [...], is handled by the entry point before either recursive walk sees it.This challenge is originally from here.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges