TypeScript Type Challenge Exclude Walkthrough
The goal of the challenge is to implement a generic type MyExclude that works the same way the built in Exclude type works, without using Exclude
The challenge: https://github.com/type-challenges/type-challenges/blob/main/questions/00043-easy-exclude/README.md
Watch the video
An example case
For example, given the following cases, our MyExclude type should return the type shown in the comments:
MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
MyExclude<'a' | 'b' | 'c', 'a' | 'b'> // 'c'
MyExclude<string | number | (() => void), Function> // string | number
Approach
In plain english, we want our type to:
loop over everything in
T
, if it doesn't exist inU
include it in our new type
On paper, this sounds very similar to our approach for implementing our own Pick type, and a naive approach might look something like:
type MyExclude<T extends (string | number | symbol), U> = {
[Property in T]: Property extends U ? never : Property
}
Where we loop over everything in T
using a mapped type, and then use a conditional type to either exclude it (returning never
) or include it (returning Property
).
This solution partially works, but results in a type (for our first example case) of:
MyExclude<'a' | 'b' | 'c', 'a'> // { a: never, b: 'b', c: 'c'}
This is along the right lines, and could probably be coerced into the expected shape, but misses the point of this challenge.
To solve this we need to know one of the "super powers" of conditional types in TypeScript, that is they are distributive over unions. What does that mean?
This means when a conditional type is working on a union type, (T extends something
where T is a union type), then the condition is applied to each member of that union.
For example,
type YesOrNo<T extends boolean> = T extends true ? "yes" : "no"
When we call that type with one value:
YesOrNo<true> // "yes"
We get back the single type "yes", but when we call it with a union type:
YesOrNo<true | false> // "yes" | "no"
We get back a union type, where each element in the union has been applied to the type.
The solution
So the solution to our MyExclude
problem actually becomes very simple:
type MyExclude<T, U> = T extends U ? never : T
We use a conditional type T extends U
to check if T
exists in U
, if it does, we return never
, otherwise we include it in the resulting object. Returning never
works to exclude items, as never
gets dropped by union types that include it.