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
}

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

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
}

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

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
}

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

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

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

I hope you found this useful!

  1. Thanks to @jazlalli1 for this suggestion!  

21