TypeScript Type Guards: An Introduction

TypeScript Type Guards: An Introduction

Type Guards are a TypeScript technique used to get information about the type of a variable, usually within a conditional or functional block. Type guards can prevent runtime errors in TypeScript by narrowing down the type of an object or variable. In this article, we discuss five ways you can use type guards in TypeScript.

The instanceof type guard

The instanceof type guard is a built-in type guard in TypeScript that can be used to check if a value is an instance of a given constructor function or class.

Here is an example of using the instanceof type guard in TypeScript:

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Dog extends Animal {
  breed: string;
  constructor(name: string, breed: string) {
    super(name);
    this.breed = breed;
  }
}

function isDog(animal: Animal): animal is Dog {
  return animal instanceof Dog;
}

const myDog = new Dog("Buddy", "Golden Retriever");
const myAnimal = new Animal("Lion");

if (isDog(myDog)) {
  console.log(`${myDog.name} is a ${myDog.breed}`);
} else {
  console.log(`${myAnimal.name} is not a dog`);
}

In this example, we define two classes Animal and Dog, where Dog extends Animal. We then define a function called isDog that takes an object of type Animal and returns a boolean value indicating whether it is an instance of the Dog class. We use the instanceof operator to perform the check. Finally, we create two objects, one of type Dog and one of type Animal, and pass them to the function to test if they are instances of the Dog class. If the object is an instance of the Dog class, we log its name and breed; otherwise, we log that it's not a dog.

typeof type guards

The typeof type guard is a built-in type guard in TypeScript that allows you to narrow down the type of a variable within a conditional block. Here is an example of using the typeof type guard in TypeScript:

function printLength(strOrNum: string | number) {
  if (typeof strOrNum === "string") {
    if (strOrNum.length > 5) {
      console.log(`The length of ${strOrNum} is greater than 5`);
    } else {
      console.log(`The length of ${strOrNum} is less than or equal to 5`);
    }
  } else if (typeof strOrNum === "number") {
    if (strOrNum > 0) {
      console.log(`${strOrNum} is a positive number`);
    } else if (strOrNum < 0) {
      console.log(`${strOrNum} is a negative number`);
    } else {
      console.log(`${strOrNum} is zero`);
    }
  }
}

printLength("hello"); // Output: The length of hello is less than or equal to 5
printLength("goodbye"); // Output: The length of goodbye is greater than 5
printLength(625); // Output: 625 is a positive number
printLength(-748); // Output: -748 is a negative number
printLength(0); // Output: 0 is zero

In this example, we define a function called printLength that takes a parameter that can be either a string or a number. We use the typeof operator to check if the parameter is a string or not. If it's a string, we check its length and log whether it's greater than or less than/equal to five. If it's not a string, we check whether it's positive, negative, or zero and log the appropriate message. By using nested conditional blocks with the typeof type guard, we can narrow down the type of the parameter and perform more specific operations on it without causing runtime errors.

The in type guard

The in type guard is used to check if an object has a certain property or not. Here is an example of using the in type guard in TypeScript:

interface Address {
  street: string;
  city: string;
  state: string;
  zipCode: number;
}

interface Person {
  name: string;
  age: number;
  address: Address;
}

function printPersonInfo(person: Person) {
  if ("name" in person && "age" in person && "address" in person) {
    const { name, age, address } = person;
    if ("street" in address && "city" in address && "state" in address && "zipCode" in address) {
      console.log(`${name} is ${age} years old and lives at    ${address.street}, ${address.city}, ${address.state} ${address.zipCode}`);
    } else {
      console.log("Invalid address object");
    }
  } else {
    console.log("Invalid person object");
  }
}

const validPerson = { name: "Tiffany", age: 30, address: { street: "123 Main St", city: "Anytown", state: "TX", zipCode: 12345 } };
const invalidPerson = { name: "Tanya", age: 25 };

printPersonInfo(validPerson); // Output: Tiffany is 30 years old and lives at 123 Main St, Anytown, TX 12345
printPersonInfo(invalidPerson); // Output: Invalid person object

In this example, we define two interfaces called Address and Person. The Address interface has four properties (street, city, state, and zipCode), while the Person interface has three properties (name, age, and address). We then define a function called printPersonInfo that takes a parameter of type Person. We use the in operator to check if the parameter has all three required properties (name, age, and address) as well as all four required properties of the nested object (street, city, state, and zipCode). If it does, we log the person's name, age, street, city, state, and zip code; otherwise, we log that the object is invalid. By using nested conditional blocks with the in type guard, we can narrow down the type of the parameter and perform more specific operations on it without causing runtime errors.

Equality narrowing type guard

The equality narrowing type guard is a technique in TypeScript that allows you to narrow down the type of a variable based on its value using the === or !== operator.

interface Person {
  name: string;
  age: number;
  address?: {
    street: string;
    city: string;
    state: string;
  };
}

function printPersonInfo(person: Person) {
  if (person.name === "John" && person.age === 30 && person.address !== undefined) {
    const { street, city, state } = person.address;
    console.log(`John is ${person.age} years old and lives at ${street}, ${city}, ${state}`);
  } else if (person.name === "Jane" && person.age === 25) {
    console.log(`Jane is ${person.age} years old`);
  } else {
    console.log("Invalid person object");
  }
}

const validPerson1 = { name: "John", age: 30, address: { street: "123 Main St", city: "Anytown", state: "TX" } };
const validPerson2 = { name: "Jane", age: 25 };
const invalidPerson = { name: "Bob", age: 40 };

printPersonInfo(validPerson1); // Output: John is 30 years old and lives at 123 Main St, Anytown, TX
printPersonInfo(validPerson2); // Output: Jane is 25 years old
printPersonInfo(invalidPerson); // Output: Invalid person object

In this example, we define an interface called Person that has three properties (name, age, and address). The address property is optional. We then define a function called printPersonInfo that takes a parameter of type Person. We use the equality operator (===) to check if the parameter has specific values for the name, age, and address properties. If it does, we log the person's name, age, street, city, and state; otherwise, we log that the object is invalid. By using nested conditional blocks with the equality narrowing type guard, we can narrow down the type of the parameter and perform more specific operations on it without causing runtime errors.

Custom type guard with predicate

A custom type guard with a type predicate is a technique in TypeScript that allows you to narrow down the type of a variable based on the result of a user-defined function. The function must return either true or false, and it should check if the value of the variable matches certain criteria. If it does, then TypeScript narrows down the type of the variable to that specific value. For example, we can use a custom type guard with a type predicate to check if an object has a certain property before performing operations on it.

To create a custom type guard with a predicate in TypeScript, we need to define a function that returns a type predicate. The function should return true or false based on whether the parameter passed to it satisfies the condition for the type guard. The syntax for defining a type predicate is parameterName is Type, where parameterName is the name of a parameter in the function signature and Type is the type we want to narrow down to.

For example, let's say we have an interface called Rectangle and we want to create a custom type guard that checks if an object is of type Rectangle. We can define our custom type guard as follows:

interface Rectangle {
  width: number;
  height: number;
}

function isRectangle(shape: unknown): shape is Rectangle {
  return typeof shape === 'object' && shape !== null && 'width' in shape && 'height' in shape;
}

In this example, we are checking if the parameter passed to the isRectangle function is an object with properties width and height. If it satisfies this condition, then it returns true and narrows down the type of shape to be of type Rectangle.

We can use our custom type guard as follows:

function calculateArea(shape: unknown) {
  if (isRectangle(shape)) {
    // TypeScript now knows that shape is of type Rectangle
    return shape.width * shape.height;
  }
  // handle other shapes
}

In this example, TypeScript now knows that if our custom type guard returns true, then the parameter passed to it must be of type Rectangle. This allows us to safely access properties like width and height without worrying about runtime errors.

Summary

In summary, type guards are used in TypeScript to get information about the data types of variables within conditional blocks. They are regular functions that return boolean values and take types as arguments. There are different ways of using Type Guards such as typeof, instanceof, and user-defined functions.