Tuple-Union conversions in TypeScript
A supplement to Array Metaprogramming in TypeScript: A fourth method to convert a union into a tuple type
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;