TypeScript Quirks: This or that, or not - type guards

Figuring out the types of things in typescript is pretty straight forward. We can tell if something is a number, or string, or undefined. We can check the instanceof classes… but for a beginner it may not be immediately obvious how we tell the different between two plain objects.

Below we have two different types of thing.

interface IThing1 {
  thingNumber: 1;
}

interface IThing2 {
  thingNumber: 2;
  foo: 'bar';
}

Okay, well that’s not a problem. But it is, when you create an array of both types of thing.

const arrayOfThings: Array<IThing1 | IThing2> = [];

If we try to access foo, which only exists on IThing2, we’ll get a type error.

const arrayOfThing2s = array.filter((thing) => thing.foo); // Property 'foo' does not exist on type IThing1

With primitives we could simply use a typeof check to ensure that our variable is of the correct type, but this doesn’t work with complex types.

const arrayOfThing2s = array.filter((thing) => {
  if (typeof thing === IThing2) { // Only works with primitives
    return true;
  }
});

With classes we could use instanceof to check if a class inherits from another, but this doesn’t work when comparing against a type.

const arrayOfThing2s = array.filter((thing) => {
  if (thing inistanceof IThing2) { // Only works with actual values (not types)
    // Also IThing1 may actually extend IThing2 and therefore be an instance of that also
    return true;
  }
});

Luckily there is a solution: type guards. Let’s make one of them now.

Here, for the sake of this example, we’ve got two types of animals in an array.

interface ICat {
  canHasCheeseburger: boolean;
  meows: true;
}

interface IDog {
  barks: true;
}

const animals: Array<ICat | IDog> = [];

If we wanted to get only the cats from our list of animals that canHasCheeseburger, we’d have to type guard the values before checking for the key canHasCheeseburger because dogs do not have a canHasCheeseburger key.

So, here’s our type guard. I’ll explain a bit more below.

const isCat = (animal: ICat | IDog): animal is ICat => (animal as ICat).meows;

There’s quite a lot going on in the above. Firstly we’re saying that this function can take either a cat or a dog, then we’re using the is keyword (which is the magic of type guards) to say that this function is definitively checking that the type of animal is ICat, and then finally we are casting our animal to be ICat so that we can access the meows key, to ensure that it is in fact a cat.

Now we can happily loop over our animals, and use our type guard to filter them and only end up with the cats that canHasCheeseburger.

const cats = animals.filter((animal) => {
  if (isCat(animal)) { // Type guard
    return animal.canHasCheeseburger; // Happy
  }

  return false;
});