Understanding TypeScripts Exclude

I recently started to do more TypeScript. I have plenty of previous experiences with typed languages but there were still some things in TypeScript that I didn't really feel comfortable with at first.

That Weird Exclude Type

While reading release notes for TypeScript 2.8 I stumbled across Omit. Not knowing what it was I set out to understand it. However, the problem grew since I found that Omit was defined as a combination of Pick and Exclude. I just couldn't for the life of me figure out what Exclude did.

Most of the articles I found about Exclude would show an example of how it was used in conjunction with another type. It felt like they sort of assumed that the reader already knew what Exclude did.

Lets Start With Union Types

So TypeScript has this awesome feature called union types. I think it is easier to show an example of a union type rather than explaining it in text.

type Language = "swedish" | "danish" | "english" | "french":

const firstLanguage: Language = "swedish";
const secondLanguage: Language = "english";

// Will not compile
const thirdLanguage = "meowing"

So in the example above we create a type called Language. A variable of type Language can now only be one of the languages we defined in the type. In this case meowing is not an acceptable language and therefore the program above will not compile.

So What Is This Exclude Thing?

This is when Exclude comes in. Exclude takes two union types and, sort of, subtracts the values in the second union type from the first union type.

type Language = "swedish" | "danish" | "english" | "french":
type NordicLanguage = Exclude<Language, "english" | "french">;

const firstLanguage: NordicLanguage = "swedish";
// This will not compile
const secondLanguage: NordicLanguage = "english";

So in the above example we create another type called NordicLanguage. This type can take on all the same values as Language except for the excluded values english and french. This is more or less the same as writing.

type Language = "swedish" | "danish" | "english" | "french":
type NordicLanguage = "swedish" | "danish";

A Cool Use Case

So I recently had a problem where I had an object that contained multiple keys of the same type. I also wanted to store which keys was currently active/selected.

As it turned out; this perfect case for Exclude.

type AvailableArea = Exclude<keyof Map, 'selectedArea'>;

type Climate = 'grass' | 'snow' | 'sand' | 'water';
interface Area {
  climate: Climate;
}

interface Map {
  selectedArea: AvailableArea;
  north: Area;
  south: Area;
  west: Area;
  east: Area;
}

The first thing that we need to understand if what keyof means.

// Same as: type keys = "selectedArea" | "north" | "south" | "west" | "east";
type keys = keyof Map;

interface Map {
  selectedArea: AvailableArea;
  north: Area;
  south: Area;
  west: Area;
  east: Area;
}

So now that we have that down the question is: Do we really want selectedArea to be able to refer to it self? In this case the answer was no. If I create a union type with the key names hard coded, what if I start adding more areas like southWest? These questions lead me to the conclusion that probably it is best if I use Exclude here.

We know that keyof returns a union type where the values can be any of the keys in the object. All we need to do now is to "exclude" selectedArea and we should be left with exactly what we want!

type AvailableArea = Exclude<keyof Map, 'selectedArea'>;

This gives me the possibility to include more areas in the future and still keep type safety throughout my application.

Closing Thoughts

Hopefully someone found this useful in some way. Next time I might cover Pick but there are plenty of tutorials out there for that and once I understood Exclude I found that Pick wasn't that hard to grasp.