A non-recursive type-level inclusion operator
type E1<X> = <T>() => T extends X ? 0 : 1
type E2<X> = <T>() => T extends X ? 0 : 1
type IsEqual<X, Y> = E1<X> extends E2<Y> ? true : false
/**
* Whether or not T includes U as an element.
*/
type Includes<T extends readonly unknown[], U> = true extends {
[key in keyof T]: IsEqual<T[key], U>
}[number] ? true : false
A non-recursive type-level Includes
operator in Typescript
Introduction
Includes
is a type-level operator that determines whether a given type T
includes a given type U
as an element. In other words, it returns true
if T
is a subtype of U
.
This operator is implemented using a simple helper function, IsEqual
, which compares two types X
and Y
and returns true
if they are equal.
Implementation
IsEqual
The IsEqual
function is implemented as follows:
type IsEqual<X, Y> = E1<X> extends E2<Y> ? true : false
where E1
and E2
are helper functions that take a type T
and return 0
if T
is equal to X
, and 1
otherwise.
Thus, IsEqual<X, Y>
returns true
if E1<X>
is equal to E2<Y>
, and false
otherwise.
E1 and E2
The E1
and E2
functions are implemented as follows:
type E1<X> = <T>() => T extends X ? 0 : 1
type E2<X> = <T>() => T extends X ? 0 : 1
These functions take a type T
and return 0
if T
is equal to X
, and 1
otherwise. These functions exploit deep behavior around generics to implement a “true type-level equality check”, which will even distinguish readonly attributes from non-readonly attributes.
The deep behavior needed to distinguish readonly attributes does not work without both type declarations, despite the equivalence. The internal type-checking behavior seems to depend on comparing two separate type identifiers.
Includes
The Includes
function is implemented as follows:
type Includes<T extends readonly unknown[], U> = true extends {
[key in keyof T]: IsEqual<T[key], U>
}[number] ? true : false
First, note that Includes
takes two type parameters: T
, which is a readonly array type, and U
, which is the element type that we want to check for inclusion in T
. We do not introduce a constraint whereby U must be an element of T, since the whole purpose of the function is to determine whether or not U is an element of T.
Next, we define a helper type, R
, which is a mapped type. This is a type whose properties are determined by mapping a given type T
to another type U
. In this case, we are mapping each element of T
to the result of IsEqual<T[key], U>
.
Thus, R
is a type with one property for each element of T
, whose value is true
if the element is equal to U
, and false
otherwise.
Finally, we return true
if R
has a property with value true
, and false
otherwise.
Advantages
The Includes
operator has a number of advantages over other implementations.
First, it is non-recursive. This means that it will not suffer from exponential typechecking, which is a problem with other implementations of Includes
.
Second, it is type-safe. This means that it will only return true
if U
is actually an element of T
. Other implementations may return true
even if U
is not an element of T
.
Third, it is efficient. This means that it will not introduce unnecessary type-checking constraints. Other implementations may introduce such constraints, which can lead to slow type-checking.
Conclusion
The Includes
operator is a simple, efficient, and type-safe way to determine whether a given type T
includes a given type U
as an element.