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