DEV Community

Cover image for Everything you need to know about Typescript Type Guards
Nikolas ⚡️
Nikolas ⚡️

Posted on • Edited on • Originally published at nikolasbarwicki.com

Everything you need to know about Typescript Type Guards

Type guards can be used to narrow down the type of any value in TypeScript. They can be used to check the type of variables, function return values, or elements in arrays, for example.

In general, type guards can be used whenever you have a value that could be of multiple types and you need to determine its actual type in order to perform specific operations on it. The type guard allows you to tell the TypeScript compiler more information about the type of a value, allowing it to provide better type checking and autocompletion in your code.

There are several built-in type guards available in TypeScript:

  1. typeof type guard: checks if a value is of a certain primitive type (e.g. number, string, boolean),
  2. in type guard: checks if a value is a property of an object or one of the properties of an union type,
  3. instanceof type guard: checks if a value is an instance of a particular class or interface,
  4. is type guard: a custom type guard that can be created by checking the shape of an object,
  5. never type guard: represents values that never occur.

typeof

The typeof operator is used to check the type of a value in JavaScript. It returns a string representing the type of the operand.

function add1000(input: string | number): number {
    if (typeof input === 'number') {
        return input + 1000;
    } else {
        return parseInt(input, 10) + 1000;
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, typeof input === ‘number’ checks if the type of input is number. If it is, the type of input within the if block will be narrowed to number, and the input + 1000 expression is valid because both operands have the same type.

typeof type guards can also be used with string literals, union types, and type aliases.

It's crucial to remember that typeof type guards are only effective when used with string, number, boolean, and symbol types. For more complex data types, you need to rely on in or instanceof type guards. The typeof type guard can identify the following data types: undefined returns "undefined", null returns "object", boolean returns "boolean", number returns "number", BigInt returns "bigint", string returns "string", symbol returns "symbol", functions (including classes) return "function", and any other object returns "object". However, for non-primitive data types, typeof type guard is less effective and instanceof or in type guards are preferred.

in

It basically solves the problem with typeof and objects. Checks if a property or method is available on an object and narrows the type of the object based on the result. For example:

interface Dog {
  move: () => void,
  bark: () => string,
}

interface Person {
  move: () => void,
  talk: () => string,
}

function makeNoise(dogOrPerson: Dog | Person): void {
  if ('bark' in dogOrPerson) {
    dogOrPerson.bark() // here only bark and move methods will be shown
  } else {
    dogOrPerson.talk() // here only methods specific for Person will be available
  }
}

Enter fullscreen mode Exit fullscreen mode

instanceof

The instanceof type guard is used to check if an object is an instance of a particular class or constructor function. The instanceofoperator is used to check if an object is an instance of a given constructor function. If the object is an instance of the constructor function, the type of the object is narrowed to be the type of the constructor function.

class Car {
  wheels: number;
  constructor(wheels: number) {
    this.wheels = wheels;
  }
}

class SportsCar extends Car {
  doors: number;
  constructor(wheels: number, doors: number) {
    super(wheels);
    this.doors = doors;
  }
}

let vehicle: Car | SportsCar = new SportsCar(4, 2);

if (vehicle instanceof SportsCar) {
  console.log(`The vehicle has ${vehicle.doors} doors.`);
}
Enter fullscreen mode Exit fullscreen mode

If the condition vehicle instanceof SportsCar is true, then the type of the vehicle variable is narrowed down from Car | SportsCar to SportsCar. This means that the TypeScript type checker now knows that the vehicle variable is of type SportsCar and allows you to access its properties and methods, such as doors, without any type errors.

The instanceof operator works by checking the prototype chain of an object. It returns true if the constructor of the prototype of the object is equal to the constructor passed as the argument. In this case, it returns true if the constructor of the prototype of the vehicle object is equal to the SportsCar constructor.

By using the instanceof type guard, you can improve the type safety of your code and avoid runtime errors. For example, if you tried to access the doors property of the vehicle object without checking its type first, you would get a runtime error if the vehicle object is actually an instance of the Car class, as it does not have a doors property. However, with the type guard in place, the TypeScript compiler will catch this error and prevent it from happening.

Another simple example checking if a value is an instance of a constructor function using Javascript Date:

const value = new Date();
if (value instanceof Date) {
  console.log(value.getFullYear());
}
Enter fullscreen mode Exit fullscreen mode

Type predicates - is

A type predicate is a special type of type guard that allows you to narrow down the type of a value based on some condition.

import axios, { AxiosError } from 'axios';

const isAxiosError = (error: unknown): error is AxiosError => {
  if (error && typeof error === 'object') {
    return 'isAxiosError' in error;
  }
  return false;
};

const fetchData = async () => {
  try {
    await axios.get('http://examole.com/api');
  } catch (error: unknown) {
    if (isAxiosError(error)) {
      console.error(`Failed with HTTP status "${error.status}`);
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Remember that you can throw any value, including a string or a number, so we want to make sure that we use properties that are present in our error object.

In this case, the type predicate is checking if the value of error is of type AxiosError by using the in operator to check if the isAxiosError property is present in the error object.

The is type guard is a way to specify that the type of the value being checked is the type specified after is. For example, error is AxiosError. If the type predicate isAxiosError returns true, the type of error is narrowed down to AxiosError within the if block, allowing you to access the properties of an AxiosError object such as status.

Without this type predicate, you would have to either throw an error or cast the value to AxiosError, both of which can be error-prone. Throwing an error would add overhead and complexity to the code, and casting the value to AxiosError would ignore the possibility that the error is not of type AxiosError.

By using the is type guard, you can ensure that the code inside the if block only runs if the error is indeed an AxiosError, avoiding the possibility of an error being thrown or any other type being used.

never

The never type in represents values that never occur. It is a bottom type, which means that it is a subtype of all other types.

A never type guard is a way to narrow down the type of an object to the never type. This is useful in cases where you want to express that a certain piece of code should never be reached, or that a certain function should never return a value.

A common use case of this type is checking all available cases with switch statement:

enum ResponseType {
  Success = "Success",
  Failure = "Failure",
  Error = "Error",
}

function handleResponse(response: ResponseType): string {
  switch (response) {
    case ResponseType.Success:
      return "The operation was successful.";
    case ResponseType.Failure:
      return "The operation failed.";
    case ResponseType.Error:
      return "An error occurred."
    default:
      const neverValue: never = response;
      throw new Error(`Unexpected response type: ${neverValue}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

The type of the variable neverValue is declared as never, which represents values that never occur. This is used to express that the default case should never be reached, as all possible values of the response parameter are covered by the switch statement.

By using the never type in this way, you can enforce type safety and prevent bugs. If a new value is added to the ResponseTypee enum in the future, and the switch statement is not updated to handle it, the default case will be triggered, and an error will be thrown with a message indicating the unexpected response type. This can help catch bugs early and improve the overall quality of your code.

It’s worth noting that in this example, the never type is not used as a type guard. Instead, it is used to explicitly declare the type of a value that should never occur. The purpose of this is to indicate to the TypeScript compiler that the default case should never be reached, and to provide a more descriptive error message in case it is reached.

Conclusion

There are several built-in type guards available in TypeScript, including typeof, in, instanceof, is, and never. The typeof type guard is used to check the type of a value and can only be used with primitive types. The in type guard is used to check if a property is available on an object and narrows the type of the object based on the result. The instanceof type guard is used to check if an object is an instance of a particular class or constructor function, and narrows the type of the object if it is. By using type guards, you can improve the type safety of their code and avoid runtime errors.

⚡️ Action Points

Top comments (0)