TypeScript Type Guards

Summary: In this tutorial, you will learn about Type Guards in TypeScript to narrow down the type of a variable.

Type Guards allow you to narrow down the type of a variable within a conditional block.

The typeof operator #

Let’s take a look at the following example:

type alphanumeric = string | number;

function add(a: alphanumeric, b: alphanumeric) {
    if (typeof a === 'number' && typeof b === 'number') {
        return a + b;
    }

    if (typeof a === 'string' && typeof b === 'string') {
        return a.concat(b);
    }

    throw new Error('Invalid arguments. Both arguments must be either numbers or strings.');
}

How it works:

  • First, define the alphanumeric type that can hold either a string or a number.
  • Next, declare a function that adds two variables a and b with the type of alphanumeric.
  • Then, check if both types of arguments are numbers using the typeof operator. If yes, then calculate the sum of the arguments using the + operator.
  • After that, check if both types of arguments are strings using the typeof operator. If yes, then concatenate the two arguments.
  • Finally, throw an error if arguments are neither numbers nor strings.

In this example, TypeScript knows the usage of the typeof operator in the conditional blocks. Inside the following if block, TypeScript realizes that a and b are numbers.

if (typeof a === 'number' && typeof b === 'number') {
    return a + b;
}

Similarly, in the following if block, TypeScript treats a and b as strings; therefore, you can concatenate them into one:

if (typeof a === 'string' && typeof b === 'string') {
    return a.concat(b);
}

The instanceof operator #

Similar to the typeof operator, TypeScript is also aware of the usage of the instanceof operator. For example:

class Customer {
    isCreditAllowed(): boolean {
        // ...
        return true;
    }
}

class Supplier {
    isInShortList(): boolean {
        // ...
        return true;
    }
}

type BusinessPartner = Customer | Supplier;

function signContract(partner: BusinessPartner) : string {
    let message: string;

    if (partner instanceof Customer) {
        message = partner.isCreditAllowed() ? 'Sign a new contract with the customer' : 'Credit issue';
    }

    if (partner instanceof Supplier) {
        message = partner.isInShortList() ? 'Sign a new contract the supplier' : 'Need to evaluate further';
    }

    return message;
}

How it works:

  • First, declare the Customer and Supplier classes.
  • Second, create a type alias BusinessPartner which is a union type of Customer and Supplier.
  • Third, declare a function signContract() that accepts a parameter with the type BusinessPartner.
  • Finally, check if the partner is an instance of Customer or Supplier, and then provide the respective logic.

Inside the following if block, TypeScript knows that the partner is an instance of the Customer type due to the instanceof operator:

if (partner instanceof Customer) {
    message = partner.isCreditAllowed() ? 'Sign a new contract with the customer' : 'Credit issue';
}

Likewise, TypeScript knows that the partner is an instance of Supplier inside the following if block:

if (partner instanceof Supplier) {
    message = partner.isInShortList() ? 'Sign a new contract with the supplier' : 'Need to evaluate further';
}

When an if narrows out one type, TypeScript knows that within the else it is not that type but the other. For example:

function signContract(partner: BusinessPartner) : string {
    let message: string;
    if (partner instanceof Customer) {
        message = partner.isCreditAllowed() ? 'Sign a new contract with the customer' : 'Credit issue';
    } else {
        // must be Supplier
        message = partner.isInShortList() ? 'Sign a new contract with the supplier' : 'Need to evaluate further';
    }
    return message;
}

The in operator #

The in operator carries a safe check for the existence of a property on an object. You can also use it as a type guard. For example:

function signContract(partner: BusinessPartner) : string {
    let message: string;
    if ('isCreditAllowed' in partner) {
        message = partner.isCreditAllowed() ? 'Sign a new contract with the customer' : 'Credit issue';
    } else {
        // must be Supplier
        message = partner.isInShortList() ? 'Sign a new contract the supplier ' : 'Need to evaluate further';
    }
    return message;
}

User-defined Type Guards #

When the type-narrowing logic is complex or needs to be reused in multiple places, you can move the check into a user-defined type guard function.

A user-defined type guard uses a special return type called a type predicate.

Here is the type predicate syntax:

parameterName is Type

For example:

function isCustomer(partner: any): partner is Customer {
    return partner instanceof Customer;
}

In this function, the type predicate is:

partner is Customer

It tells TypeScript that if isCustomer(partner) returns true, treat partner as a Customer. The name before is must match the name of a function parameter.

The partner is Customer annotation provides additional information to the TypeScript compiler so that it can narrow the type in the calling code.

You can use the type guard in an if statement:

function signContract(partner: BusinessPartner): string {
    let message: string;
    if (isCustomer(partner)) {
        message = partner.isCreditAllowed() ? 'Sign a new contract with the customer' : 'Credit issue';
    } else {
        message = partner.isInShortList() ? 'Sign a new contract with the supplier' : 'Need to evaluate further';
    }

    return message;
}

Inside this block:

if (isCustomer(partner)) {
  // partner is Customer
}

TypeScript narrows partner from:

Customer | Supplier

to:

Customer

Therefore, you can safely call the Customer method:

partner.isCreditAllowed();

In the else branch, TypeScript knows that partner must be a Supplier:

else {
  // partner is Supplier
  partner.isInShortList();
}

Type Predicate vs. Boolean Return Type #

The following isCustomer function returns true if a partner is a customer:

function isCustomer(partner: BusinessPartner): boolean {
  return partner instanceof Customer;
}

The function answers the runtime question, but its return type (boolean) does not explicitly describe the intended narrowing relationship.

A type predicate communicates that relationship:

function isCustomer(
  partner: BusinessPartner
): partner is Customer {
  return partner instanceof Customer;
}

Therefore:

  • boolean means the function returns true or false.
  • partner is Customer means the function returns a boolean and, when the result is true, partner can be treated as a Customer.

Summary #

  • Type guards narrow down the type of a variable within a conditional block.
  • Use the typeof and instanceof operators to implement type guards in conditional blocks

Was this helpful?