Type Guards in Typescript

Often times when I speak to developers who have just started writing typescript, they have begun to understand static typing. They understand that they can label a variable as type string and then rely on the fact that it will be a string within a function. There is however, another, rather large part of typescript that I don't see newer typescript devs use very often. Type Guards and their ability to branch code in the event a function takes multiple types are extreemly useful. So today, I'm going to explain Type Guards and why you should be using them.

A Type Guard is a special sort of function. It allows you to pass a value and receive a boolean which is true if the passed value is of that type. In javascript, Array.isArray is a perfect example. Typescript would consider it a Type Guard as it returns true in the event it is passed an Array.

To begin our explanation, let's say that we have a base interface of Animal and we are extending it for the interfaces of Cat and Dog.

As you can see in our Animal interface, species is a string. However, when extended we've explicitly defined that species will either by Cat or Dog. In the real world, it's unlikely that two extended interfaces would have a shared field with an explicit value like this, but for our example, it's going to help explain a few things.

We can write a function to accept the Animal interface, but because both Cat and Dog extend that interface, they are possible to pass as well. Let's create our basic function.

If we create a Cat and pass it in, you can see that no errors are thrown because Cat is an extension of Animal.

Now pretend that we want to add some specific logic if animal is a Dog. We could start off by assuming that every passed in Animal is a Dog. However, as you'll see with the below update to aboutAnimal, You will get the error Property 'ownerName' does not exist on type 'Animal'.

To fix this, we can create a Type Guard. Type Guards use the is keyword in their return type to indicate if the value is of a specifc type. They can contain whatever logic you need to confirm with certianty that the type is what you are expecting. Let's create a Type Guard for Dog

This might seem reasonable as a Type Guard. You are checking the species is Dog which is a key indicator of the type being Dog. Unfortunately, it is possible that someone made a mistake elsewhere in your code and created an instance of the parent interface Animal with the species set to Dog. With this Type Guard, you cannot be sure that ownerName is not still undefined.

To fix this problem, we need to look at what fields are unique within the interface Dog and instead check for those. Luckily, cats view their owners as lesser (I have cats don't come for me) and as such while a Dog has a ownerName field, a Cat instead has a petHumanName so we can check for that. With our current intefaces, only Dog has the ownerName field.

Much better. If we now use that Type Guard in our about animal function. We'll see our error disappear as the code trusts the Type Guard to ensure that only Dog values get through.

Type Guards are incredibly useful when you have functions that take extended interfaces or even multiple types ( eg. (val: string | number) => {} ) and you need to have branching logic depending on what's passed in. Especially if the types involved are complicated or you'd need to check multiple fields to confirm the type.

You can use the bang operator (!) to negate the check and instead check if a value is not the given type (eg. Not a Dog). This can be useful if you need to perform a type conversion to ensure type consistancy.

Let me know how you find using Type Guards in the comments or by @ing me on twitter @mikevdev. Make sure to share this article with anyone you think might need to learn about Type Guards.

Michael Walter Van Der Velden
Loading Comments