Tuple-Union conversions in TypeScript

A supplement to Array Metaprogramming in TypeScript: A fourth method to convert a union into a tuple type

Matthias Falk
4 min readOct 6, 2021

Preliminaries

Conditional types sometimes have else branches that are never entered. Nevertheless one has to fill in some type in such a branch. Normally never is chosen for that purpose. I have picked up the idea to alias this usage of never as Impossible from here: type Impossible = never. Moreover, to signal that some inadmissible type parameter has been fed to a generic type, I use type InvalidTypeParameter = null to signal that.

In order to test our conversion types later, we use a generic type EQ that takes two type parameters and evaluates to true, roughly if the given types are equal (but see Type equivalence in TypeScript is tricky for the intricacies):

type EQ<A, B> =
[A] extends [B]
? ([B] extends [A] ? true : false)
: false;

Solution 4: Convert a union into a tuple type

The algorithmic idea is the following: Pick some component type from the union, put it as first or last element into a tuple and fill the rest of the tuple recursively with the elements of the tuple that is made from the rest of the components: Let U1 | ... | Un be the union tuple type. Then the core idea is to construe Tuplify<U1 | ... | Un> as [U1, ...Tuplify<U2 | ... | Un>].

The core difficulty is to pick a component from the union. This can be done with — I would say — a very dirty trick described here (credit to the author, not to me). It works as follows: TypeScript interprets an intersection type (A1 => R1) &…& (An => Rn) of function types as a single overloaded function type and extracts, when “used” in a conditional type, just one of these overloads. Which one is not specified (while it is normally the last one given). This trick might not work for all future versions of TypeScript.

Our first auxiliary type UnionToUnionOfZeroAryFunctionTypes converts a finite union of types into a union of zero-ary function types with the union components as return types. Example: UnionToUnionOfZeroAryFunctionTypes<'a' | 'b' | 5> evaluates to (() => 'a') | (() => 'b') | (() => 5):

type UnionToUnionOfZeroAryFunctionTypes<FiniteUnion> =
FiniteUnion extends unknown
? () => FiniteUnion
: Impossible;

Note that this type makes use of TypeScript’s distribution mechanism.

The next auxiliary type UnionOfZeroAryFunctionsToIntersection converts a finite union of zero-ary function types into an intersection of these function types. Note that the resulting intersection type is interpreted by TypeScript as a single overloaded function type. Example: UnionOfZeroAryFunctionsToIntersection<(() => 'a') | (() => 'b') | (() => 5)> evaluates to (() => 'a') & (() => 'b') & (() => 5):

type UnionOfZeroAryFunctionsToIntersection<UnionOfZeroAryFunctions> = ( UnionOfZeroAryFunctions extends unknown
? (k: UnionOfZeroAryFunctions) => void
: Impossible
) extends((k: infer FunctionsIntersection) => void)
? FunctionsIntersection
: Impossible;

Now we can apply the dirty trick:

type GetSomeFiniteUnionComponentByDirtyTrick<FiniteUnion> =
UnionOfZeroAryFunctionsToIntersection<UnionToUnionOfZeroAryFunctionTypes<FiniteUnion>> extends (() => (infer R))
? R
: InvalidTypeParameter;

For example, GetSomeFiniteUnionComponentByDirtyTrick<'a' | 'b' | 5> evaluates to 5, to 'a' or to 'b'. It is not specified to which one.

After this difficult preparation the recursive type TuplifyUnion is straightforward:

type TuplifyUnion<U> =
GetSomeFiniteUnionComponentByDirtyTrick<U> extends infer Element
? ([U] extends [never]
? []
: (TuplifyUnion<Exclude<U, Element>> extends infer RestTuple
? (RestTuple extends unknown[]
? [Element, ...RestTuple]
: Impossible)
: Impossible))
: Impossible;

Convert a tuple into a union type

The algorithmic idea is again a simple recursion: Take the first tuple element type and unify it with the unification of the rest of the tuple:

type TupleToUnionBase<T> =
T extends []
? never
: (T extends [infer First, ...infer Rest]
? First | TupleToUnionBase<Rest>
: Impossible);

However, due to very restrictive “design limitations” concerning the allowed nesting depth of recursive types, I have construed an ugly workaround to stretch these “design limitations” a bit:

type TupleToUnion<T> =
T extends [infer First, infer Second, infer Third, infer Fourth,
infer Fifth, ...infer Rest]
? First | Second | Third | Fourth | Fifth | TupleToUnion<Rest>
: TupleToUnionBase<T>;

Testing our conversion types

We make use of the fact that TuplifyUnion and TupleToUnion are (roughly) inverses of each other. Roughly only since TuplifyUnion guarantees no particular order of the tuple components:

const testTuplifyUnion1:
EQ<TupleToUnion<TuplifyUnion<8 | 3 | 1 | 0 | 16 | 27 | 2 | 9 | 33 |
22>>,
8 | 3 | 1 | 0 | 16 | 27 | 2 | 9 | 33 | 22> = true;
const testTuplifyUnion2:
EQ<TupleToUnion<TuplifyUnion<8 | 3 | 1 | 0 | 16 | 27 | 2 | 9 | 33 |
22 | 129 | 32 | 19 | 4 | 11>>,
8 | 3 | 1 | 0 | 16 | 27 | 2 | 9 | 33 | 22 | 129 | 32 | 19 | 4 |
11> = true;
const testTuplifyUnion3:
EQ<TupleToUnion<TuplifyUnion<8 | 3 | 1 | 0 | 16 | 27 | 2 | 9 | 33 |
22 | 129 | 32 | 19 | 4 | 11 |
boolean | string>>,
8 | 3 | 1 | 0 | 16 | 27 | 2 | 9 | 33 | 22 | 129 | 32 | 19 | 4 |
11 | boolean | string> = true;

--

--

Responses (1)