How to use Type Guards in Typescript

yoniweisbrod

Yoni Weisbrod

Posted on December 21, 2021

How to use Type Guards in Typescript

One case where people are often surprised by TypeScript is where there's a union type (a type comprised of multiple other types). It can be confusing when you've clearly differentiated between the two types in the code - but the compiler can't tell the difference.

For example, say we have an Animal that can be either Human or Dog, and we want to implement a speak function:

type Human = { talk: () => void }
type Dog = { bark: () => void }

type Animal = Human | Dog;

function speak(animal: Animal) {
    // if human -> talk
    // if dog -> bark
}
Enter fullscreen mode Exit fullscreen mode

How do we differentiate between Dog and Human here? TS types aren't available at runtime, so we can't check the types. But what we can do is use certain properties as indicators of the type:

function speak(animal: Animal) {
    if (animal.talk) {
        animal.talk()
    } else {
        animal.bark()
    }
}
Enter fullscreen mode Exit fullscreen mode

The problem is that this won't work for the TS compiler. As far as it's concerned, animal.talk is an invalid access of a property since Animal may be Dog or Person, and a Dog cannot talk.

So TS lets you tell it which of the subtypes it is by using a function that returns a type predicate indicating whether or not the argument is a certain type. A type predicate always uses the is syntax:

function isHuman(animal: Animal): animal is Human {
    return !!animal.talk
}
Enter fullscreen mode Exit fullscreen mode

The function itself is called a type guard. If it returns true, the type predicate will be true and TS will understand that animal is a Human subtype of Animal.

Let's swap it in:

function speak(animal: Animal) {
    if (isHuman(animal)) {
        animal.talk()
    } else {
        animal.bark()
    }
}
Enter fullscreen mode Exit fullscreen mode

However, we still suffer from the same problem as above: animal.talk can be either Dog or Human within the type guard. And so to solve it we can cast animal as Human within the type guard:

function isHuman(animal: Animal): animal is Human {
    return !!(animal as Human).talk
}
Enter fullscreen mode Exit fullscreen mode

or remove the dot access entirely using the Javascript in operator:1

function isHuman(animal: Animal): animal is Human {
    return 'talk' in animal;
}
Enter fullscreen mode Exit fullscreen mode

And finally:

type Human = { name: string; talk: () => void }
type Dog = { name: string; bark: () => void }

type Animal = Human | Dog;

function isHuman(animal: Animal): animal is Human {
    return 'talk' in animal;
}

function speak(animal: Animal) {
    if (isHuman(animal)) {
        animal.talk()
    } else {
        animal.bark()
    }
}
Enter fullscreen mode Exit fullscreen mode

I hope you found this useful!


  1. Thanks to @jazlalli1 for this suggestion!  

💖 💪 🙅 🚩
yoniweisbrod
Yoni Weisbrod

Posted on December 21, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related