#869Extreme

DistributeUnions

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>.

Challenge Instructions: DistributeUnions

Extreme

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  } }]
//      | 17

For 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  } }]
//      | 17

View on GitHub: https://tsch.js.org/869

Change the following code to make the test cases pass (no type check errors).

ChallengeSolution
/* _____________ 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<
 

Pro Challenge

Unlock 150+ medium, hard, and extreme challenges to master advanced TypeScript.

Monthly subscription. Cancel anytime.

Detailed Explanation

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
  : never

The entry point does the top-level work for free

T 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.

Tuples: one element at a time into an accumulator

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.

Objects: peel one key, then recurse on the rest

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.

Flatten keeps the result comparable

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' }>    is

Edge cases the tests cover

This challenge is originally from here.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges