TypeScript Type Guards

May 18, 2023#typescript

TypeScript type guards are expressions that perform a runtime check on a value and narrow its type within a conditional block. They are useful when you want to discriminate between different types of values and access their specific properties or methods.

There are different ways to write type guards in TypeScript, depending on the kind of types you want to check.

Using typeof operator

This is a built-in operator that returns the type of a value as a string. You can use it to check for primitive types such as number, string, boolean, etc. For example:

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
    // padding is narrowed to number
    return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
    // padding is narrowed to string
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${typeof padding}'.`);
}

In this example, the typeof operator acts as a type guard for the padding parameter, which can be either a string or a number. Within each if block, TypeScript knows that padding has a specific type and allows you to use its methods.

Using instanceof operator

This is another built-in operator that checks if a value is an instance of a class or constructor function. You can use it to check for object types that have a prototype chain. For example:

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

class Dog extends Animal {
  bark() {
    console.log("Woof! Woof!");
  }
}

class Cat extends Animal {
  meow() {
    console.log("Meow! Meow!");
  }
}

function greet(animal: Animal) {
  console.log(`Hello, ${animal.name}!`);
  if (animal instanceof Dog) {
    // animal is narrowed to Dog
    animal.bark();
  }
  if (animal instanceof Cat) {
    // animal is narrowed to Cat
    animal.meow();
  }
}

In this example, the instanceof operator acts as a type guard for the animal parameter, which can be an instance of Animal or any of its subclasses. Within each if block, TypeScript knows that animal has a specific type and allows you to use its methods.

Using in operator

This is another built-in operator that checks if a property exists on an object or its prototype chain. You can use it to check for object types that have different sets of properties. For example:

interface Fish {
  swim: () => void;
}

interface Bird {
  fly: () => void;
}

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    // animal is narrowed to Fish
    animal.swim();
  } else {
    // animal is narrowed to Bird
    animal.fly();
  }
}

In this example, the in operator acts as a type guard for the animal parameter, which can be either a Fish or a Bird. Within each branch, TypeScript knows that animal has a specific type and allows you to use its properties.

Using user-defined type guard

These are custom functions that return a boolean value based on some condition. You can use them to check for any kind of types that are not easily distinguished by the built-in operators. For example:

interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

type Shape = Circle | Square;

function isCircle(shape: Shape): shape is Circle {
  return shape.kind === "circle";
}

function getArea(shape: Shape) {
  if (isCircle(shape)) {
    // shape is narrowed to Circle
    return Math.PI * shape.radius ** 2;
  } else {
    // shape is narrowed to Square
    return shape.sideLength ** 2;
  }
}

In this example, the isCircle function acts as a user-defined type guard for the shape parameter, which can be either a Circle or a Square. The function uses a type predicate shape is Circle to tell TypeScript that if it returns true, then shape has the Circle type. Within each branch, TypeScript knows that shape has a specific type and allows you to use its properties.