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;
});