TypeScript Type Challenge Exclude Walkthrough

| 2 min read

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 in U 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.