Extension of Constraining TypeScript’s type parameters to “ground” instantiations: Accepting also implicit components and checking types for being union types

This is an extension of How to constrain in TypeScript type parameters to “ground” instantiations of union types or its simplification, respectively.

Matthias Falk
3 min readAug 4, 2021

Within How to constrain in TypeScript type parameters to “ground” instantiations of union types (or within its simplification) we have construed a type

type IsOneOf<T, Union> =
true extends EqualsOneOfTheComponents<T, Union>
? true
: false;

that checks a union type Union for whether T equals one of its components. In particular, it evaluates to false in case T is instantiated by a (not necessarily proper) sub-union of Union.

Examples: For type Union = U1 | U2 | … | Un for we get IsOneOf<U2, Union> -> true and IsOneOf<U1 | U3, Union> -> false.

We have distinguished explicit from implicit components:

Explicit components are those listed when creating a union type (remember that TypeScript’s boolean behaves as if it were defined as type boolean = true | false). So, the explicit members of type Union above are basically the U1,…Un, but not exactly: If some of the Ui are union types themselves, then the explicit “ground” components which are checked with IsOneOf are the explicit “ground” components of those Ui. Thus, the definition applies recursively.

Implicit components are the (at least conceptually) infinitely many ones of the inbuilt types number and string. numbers interpreted set-theoretically is the infinite union of all numbers, the respective interpretation holds for string. For immediately obvious reasons TypeScript cannot apply its distribution mechanism for infinitely many components. Thus, we have to modify the algorithmic idea of checking for explicit components which was: Let TypeScript dissect the union via its distribution mechanism into its components and test each of them with the type to be checked for equivalence. The modification’s core idea is: Just test for subtypehood, rather than for equivalence: Remember that we have used for IsOneOf the auxiliary type EqualsOneOfTheComponents which we had defined as

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

The equivalence check was performed by type EQ. So, basicly we replace it by a bare check for subtypehood:

type ExtendsOneOfTheComponents<T, Union> =
Union extends infer Component // enforce distribution
? ([T] extends [Component]
? true
: false)
: Impossible;

and modify IsOneOf to IsOneOfImplicitOrExplicitComponents: (attention, this version is not final yet):

type IsOneOfImplicitOrExplicitComponents<T, Union> =
true extends ExtendsOneOfTheComponents<T, Union>
? true
: false;

While IsOneOf<'a', string | boolean> evaluates to false as intended, IsOneOfImplicitOrExplicitComponents<'a', string | boolean> evaluates to true, also as intended. However, IsOneOfImplicitOrExplicitComponents<'a' | 'A', string | boolean> also evaluates to true, what is unintended since we wanted to allow only single components, not unions.

How to fix?

We check an arbitrary type whether it is a union type or not, i.e. we construe a type IsProperFiniteUnion<T> that evaluates to true if T is a union type and to false otherwise.

The core idea: We feed the given type T into an auxiliary type simultaneously two times and let TypeScript dissect one instance via its distribution mechanism and leave the other instance as it is. We check whether the undissected instance extends the components of the dissected instance. If T is a (proper, non-degenerated) union type, then (set-theoretically spoken) the undissected instance is a proper superset of each of its components. However, if T is not a union type, i.e. if it consists of just a single (explicit) component, then this single component equals the undissected instance. This is the difference that we exploit.

We start with the auxiliary type which looks quite unintuitive if considered isolated from its intended usage:

type
UnionOfTruthValuesForUnionNotExtendsComponent<ToBeDissected,
RemainsUndissected>
= ToBeDissected extends infer Component // dissecting
? ([RemainsUndissected] extends [Component]
? false
: true)
: Impossible;

UnionOfTruthValuesForUnionNotExtendsComponent<3 | 5, 3 | 5>, e.g., evaluates to true while UnionOfTruthValuesForUnionNotExtendsComponent<3 , 3>, e.g., evaluates to false.

So we are ready for IsProperFiniteUnion

type IsProperFiniteUnion<T> = 
UnionOfTruthValuesForUnionNotExtendsComponent<T, T>;

and for our final version of IsOneOfImplicitOrExplicitComponents:

type IsOneOfImplicitOrExplicitComponents<T, Union> =
IsProperFiniteUnion<T> extends true
? false
: true extends ExtendsOneOfTheComponents<T, Union>
? true
: false;

TypeScript Playground

--

--