TypeScript Type Challenge Includes Walkthrough
The goal of the challenge is to implement a generic type Includes<T extends unknown[], U>
that returns true if U
is included in T
.
The challenge: https://github.com/type-challenges/type-challenges/blob/main/questions/00898-easy-includes/README.md
Watch the video
Example "easy" cases
The easier examples to solve this problem for are shown below:
For the given examples, our Includes type should return the type as shown in comment
Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Kars'> // true
Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // false
Includes<[1, 2, 3, 5, 6, 7], 7> // true
Includes<[1, 2, 3, 5, 6, 7], 4> // false
Includes<[1, 2, 3], 2> // true
Includes<[1, 2, 3], 1> // true
Includes<[false, 2, 3, 5, 6, 7], false> // true
Includes<[null], undefined> // false
Includes<[undefined], null> // false
These examples are all fairly simple, they include an array of types as T
and one specific type as U
. If U is inside T
we return true, otherwise we return false.
More complex cases
Includes<[{}], { a: 'A' }> // false
Includes<[boolean, 2, 3, 5, 6, 7], false> // false
Includes<[true, 2, 3, 5, 6, 7], boolean> // false
Includes<[{ a: 'A' }], { readonly a: 'A' }> // false
Includes<[{ readonly a: 'A' }], { a: 'A' }> // false
Includes<[1], 1 | 2> // false
Includes<[1 | 2], 1> // false
In these more complicated cases, the array T
includes complex types such as objects and type classes. For example in the second complex case, false
is assignable to boolean
but not equal to it, so false
is not inside T
despite boolean
being a member of T
.
Approach
Lets forget the complex use example cases for now. If you've been following the challenges, you may have taken the excludes challenge already. This challenge seems very similar although this time we want to return true if U
is in T
, not the other way round.
We know that working with tuple types when doing type manipulation can be simplified by turning a tuple into a union type like so:
T[number]
And once we've turned our tuple type into a union type we can simply ask if U
is assignable to it and write a simple type like so:
type Includes<T extends readonly unknown[], U> = U extends T[number] ? true : false
This converts our type T
to a union of all the members of the tuple, then checks to see if U
is assignable to to it. In this case, assignable to means "is equal to". If U
is, return true, else return false.
This works for the simple cases, but not the more advanced cases. Lets dig into these to find out why
A harder example
For this example,
Includes<[boolean, 2, 3, 5, 6, 7], false>
Our type is currently returning true, but it should be returning false. Clearly false
isn't in our array, but the Includes type thinks it is. What's going on here?
The problem is with our equality check, we're asking our union type if false
is assignable to it. Well it is! false
is assignable to boolean
, as is true
. Clearly we need a better way of comparing types.
The Type Challenge Utilities
The type challenges come with a set of utility types that are used to make assertions in the playground. One of these types is called Equal. We won't go into how this type works in this post, but know that you can use it to compare the equality of two types:
Equal<true, boolean> // returns false
Equal<false, boolean> // also returns false
Now all we have to do is make use of this type in our Includes
type
The solution
To solve this challenge we need to abandon our naive solution and rethink the approach completely. Instead of converting our array into a union type, we're instead going to loop over the elements, and compare the equality of each one to U
using our Equal
type.
Unfortunately there's no looping construct when doing type manipulation, instead we'll have to rely on recursion. Our approach will be:
- Grab the first element from the array call it
First
- Grab everything else out of the array and call that
Rest
- Compare
First
toU
usingEqual
- If they're equal, return true
- If not, call our
Includes
type again withRest
and the existingU
- If we reach the end of the array, return false as we haven't found our element
Using Infer to 'destructure' our array
In my opinion, the hardest part of the steps above is working out how we can grab the first element, and the rest of the elements out of the T
array.
To do this, we can use the infer
keyword. We can use this in a condition type like so:
T extends [infer First, ...infer Rest] ? true : false
infer First
is bound to the first element in the array, ...infer Rest
is bound to everything else in the array, or the empty array if there are no other elements. This works very similarly to how it would in JavaScript.
TypeScript will only match infer First
if there is at least one element in the array, i.e. there is a type for typescript to infer. This means that the condition will return false when we pass the empty array. If there is only one element in the array ...infer Rest
will bind to []
, much like the spread operator in JavaScript.
The final solution
Turning the steps into code leaves us with:
type Includes<T extends readonly unknown[], U> = T extends [infer First, ...infer Rest]
? Equal<First, U> extends true
? true
: Includes<Rest, U>
: false
We need the additional conditional type to compare the equality of First
and U
to true.
The recursive nature of this means the T
will become one element smaller each time we call it, eventually becoming []
(if we don't find U
and First
). If T
does become []
this is our base case of our recursive type, and we return false
as we fail the initial conditional check.