How to constrain in TypeScript type parameters to “ground” instantiations of union types

Matthias Falk
4 min readAug 4, 2021

Problem

We consider the situation where you have a generic type type T<U> = … for a union type U = U1 | U2 | … | Un in which you intend to make the evaluation result of a type instantiation T<…> dependent on the instantiation of U . Say, you want T<Ui> to evaluate to Vi for i{1,…,n} , i.e. you want to use U as a switch.

The first impulse is to constrain the type parameter like this: type T<W extends U> = … . While there is nothing wrong with this constraint, it is just not sufficient for to stay on the safe side for the intended purpose. What slips through the safety net is a union subtype instantiation of the parameter, e.g. T<U1 | U3> . This problem is less artificial as it seems at first glance in case the type parameter itself is the result of a more or less complex type evaluation, say T<Y<S1,…,Sm>> where it is not immediately obvious to which result type the instantiation Y<S1,…,Sm> evaluates. Therefore, we would like to have a possibility to (type) programmatically validate T ‘s type parameter.

A special case that might occur more often is U = boolean . If you want to use U as a switch, you want to instantiate it with either true or false but not with true | false (what is TypeScript’s internal represenation of boolean ).

Some concretization of what we want to achieve: Ideally, we want to enforce a TypeScript compiler error for inadmissible parameter instantiations, such as it occurs e.g. for instantiation T<'5'> if T is defined as type T<W extends number> = … . The second best thing is to make the instantiation evaluate to some error type such that a subsequent error occurs if you want to use such a faulty instantiation. I use the alias type InvalidTypeParameter = … for an error type and type Impossible = … for branches of conditional types that are not entered on any circumstances. I have picked up this idea here: Advanced TypeScript Type Tricks. In particular I use type InvalidTypeParameter = null and type Impossible = never but appropriate alias targets might vary from case to case.

Thus, what we want to achieve is that T<U1 | U3> evaluates to InvalidTypeParameter in case T is defined as above as type T<W extends U> = … for type U = U1 | U2 | … | Un and the parameter should be used as a switch, i.e. an admissible instantiation of W should be exactly one of the Ui s for i{1,…,n}.

Let’s consider first the special case of U = boolean for type T<W extends U> = … . Say, we want T<true> to evaluate to V1 and T<false> to V2. A solution would be

type T<W extends boolean> =
[W] extends [true]
? V1
: [W] extends [false]
? V2
: InvalidTypeParameter;

To make the solution generic we want to construe a type IsOneOf<T, Union> = … that evaluates to true if T is instantiated with some Ui for i{1,…,n} and type Union = U1 | U2 | … | Un and to false otherwise. We would use this type then in this way:

type T<W extends U> =
IsOneOf<W, U> extends true
?
: InvalidTypeParameter;

The main problem is to disassemble some unrestricted type U considered a union type into its explicit components. Note that a non-union type as, say, type NameIdentifier = 'name' can be considered a degenerated union type with just one component. Note also the distinction between explicit and implicit components: The explicit components of type ValidNumbers = 1 | 2 | 3 are 1, 2 and 3, while the implicit (and at least conceptually infinitely many) components of number are all the numbers. We are interested in the explicit components only which are automatically only finitely many (keep in mind the special case boolean which consists of exactly the two explicit components true and false).

Fortunately TypeScript’s distribution mechanism (see TypeScript 2.8: section Distributive conditional types) is exactly the disassembling mechanism we are looking for. A first auxiliary type EQ that we need checks for type coextensionality (which basicly means equivalence, i.e. you can freely interchange equivalent types but see Type equivalence in TypeScript is tricky for the subtleties):

type EQ<A, B> =
[A] extends [B] // prevent distribution by wrapping into []
? ([B] extends [A] ? true : false)
: false;

An instantiation EQ<U,V> evaluates to true if the types U and W (considered mathematical sets of their admissible values, respectively) are equal, and to false otherwise. For example, EQ<boolean, true | false> evaluates to true .

We use EQ to check whether a type T equals at least one of the explicit components of a (possibly degenerated) union type Union:

type EqualsOneOfTheComponents<T, Union> =
Union extends infer Component // enforce distribution
? EQ<T, Component>
: Impossible;

A trick here is to enforce the disassembling of type Union with the pseudo condition Union extends infer Component. It is a pseudo condition since the false branch is never entered. Since Union is a bare type parameter, TypeScript applies its distribution mechanism and with the infer Component part we can address an explicit component of Union by the name Component within the subsequent code. Thus, for type Union = U1 | U2 | … | Un EqualsOneOfTheComponents<T,Union> evaluates to EQ<T,U1> | EQ<T,U2> | … | EQ<T,Un>.

Our next auxiliary type is

type IsMemberOrSubtypeOfAComponent<T, Union, ConjunctionOfExplicitComponentChecks extends boolean> =
[T] extends [Union]
? (true extends ConjunctionOfExplicitComponentChecks
? true
: false)
: false;

We intend to instantiate the type parameter ConjunctionOfExplicitComponentChecks with the evaluation result of an appropriate instantiation of EqualsOneOfTheComponents. The first condition [T] extends [Union] just checks whether T is a subtype of Union or not. If this is the case, it checks next whether true belongs to the components of ConjunctionOfExplicitComponentChecks (remember that the latter can evaluate to either true, false or boolean).

Finally, our target type unfolds as

type IsOneOf<T, Union> = 
IsMemberOrSubtypeOfAComponent<T, Union,
EqualsOneOfTheComponents<T,
Union>>;

(in TypeScript Playground)

See simplification.

--

--