Mastering Type Guards in TypeScript: Making Your Code Safer

Robin
Updated on March 17, 2023

Type guards in TypeScript are a powerful feature that allows developers to narrow down the type of a variable at runtime. You should use type guards to ensure type safety in your code.

In this post, we will explore the different ways to use type guards in TypeScript and discuss how they can help make your code safer and more reliable.

By using type guards, developers can improve the quality of their code and reduce the risk of runtime errors caused by unexpected types.

Understanding Type Guards in TypeScript

Type guard is a process of checking the type of a variable in TypeScript. There are different ways to implement type guards, including using the typeof, instanceof, and in operators or creating user-defined type guards.

Type guards are particularly useful when working with union types, third-party libraries, user input validation, and complex data structures.

There are 5 types of type guards in TypeScript:

  • The typeof Type Guard
  • The in Type Guard
  • The instanceof Type Guard
  • Equality Narrowing Type Guard
  • Custom User-Defined Type Guard

We will discuss each of these techniques in detail with examples.

Using the typeof Type Guard in TypeScript

The typeof operator is a built-in type guard in TypeScript that checks the type of a variable at runtime. You have to use this keyword in front of a variable name.

It returns a string that represents the type of the variable, allowing developers to narrow down the type of the variable in a conditional block.

          const firstName = 'Max'

typeof firstName
// It will return "string"
        

One of the most common use cases for the typeof type guard is checking for primitive types such as number, string, boolean, undefined, function, object, and symbol.

You can check whether the type of a variable matches the type that you want. Based on the condition, we can perform some actions.

          const printInfo = (value: string | number) => {
    if (typeof value === 'number') {
        console.log(`Student ID: ${value}`)
    } else {
        console.log(`Student name: ${value}`)
    }
}

printInfo(23)
// Student ID: 23

printInfo('Max')
// Student name: Max
        

In this example, I have a function called printInfo that takes one parameter. The value parameter can be either a string or a number.

Inside the function, you can check the type of this parameter using typeof type guard to determine whether it is a number or a string.

If value is a number, the function prints out a message indicating that it is a student ID, along with the actual value of value.

On the other hand, if value is a string, the function prints out a message indicating that it is a student name, along with the actual value of value.

Using the in Operator as a Type Guard

In TypeScript, you can use the in operator as a type guard to determine whether an object has a certain property or index signature. This will return a boolean value.

If the property exists in that object, it will return true otherwise false. That's why we can use this operator with conditional statements.

Let's consider a function that takes one parameter which can hold 2 types of objects. You want to perform different operations depending on the object type.

          interface Circle {
    kind: 'circle'
    radius: number
}

interface Square {
    kind: 'square'
    sideLength: number
}

type Shape = Circle | Square

const calculateArea = (shape: Shape) => {
    if ('radius' in shape) {
        // shape is now narrowed down to Circle type
        return Math.PI * shape.radius ** 2
    } else {
        // shape is now narrowed down to Square type
        return shape.sideLength ** 2
    }
}
        

I have two interfaces Circle and Square that describe the properties of a circle and a square respectively. The calculateArea function takes in a shape parameter that can either be a Circle or a Square.

Now you can check whether the radius property is present in the shape object or not using the TypeScript in operator. If it exists, that means the shape is a Circle.

You can use the formula to calculate the area of this circle. If the radius property doesn't exist then the shape is a square. Use a different formula to calculate its area.

One limitation of this method is that you have to type the property names as a string. TypeScript will not be able to give you suggestions. You might type the wrong property name.

Also Read: Best Ways to Access Object Properties Dynamically in Javascript

Using the instanceof Operator as a Type Guard

The instanceof operator in TypeScript is a guard that allows us to check if an object is an instance of a particular class or constructor function.

If the object is an instance of the class, it will return true, otherwise false. It will match when the object contains similar properties and methods of that specific class or constructor function.

          class Car {
    startEngine() {
        console.log('Starting car engine...')
    }
}

class Truck {
    startEngine() {
        console.log('Starting truck engine...')
    }

    loadCargo() {
        console.log('Loading cargo...')
    }
}

const useVehicle = (vehicle: Car | Truck) => {
    vehicle.startEngine()

    if (vehicle instanceof Truck) {
        vehicle.loadCargo()
    }
}

const car = new Car()
const truck = new Truck()

useVehicle(car)
// Starting car engine...

useVehicle(truck)
// Starting truck engine...
// Loading cargo...
        

In this example, I have two classes, Car and Truck. Both classes have a startEngine method, but only the Truck class has an additional loadCargo method.

The useVehicle that takes in a parameter vehicle which can be either a Car or a Truck. Inside this function, we call the startEngine method on the vehicle parameter, regardless of whether it is a Car or a Truck. Because both of them have this method.

But if you try to call the loadCargo method, TypeScript will give an error. Because this method is only available in the Track class. That's why you have to check whether the parameter vehicle is an instance of the Truck class using the instanceof operator.

Equality Narrowing (Literal) Type Guard

As you know when a variable stores multiple types of objects, you need to use a type guard to ensure that you are accessing properties from the correct object. A common property name can be used as a type guard to identify the correct object type.

You have to provide a unique value for this common property. Unlike the in operator, you will get TypeScript suggestions in this technique.

          interface Circle {
    kind: 'circle'
    radius: number
}

interface Square {
    kind: 'square'
    sideLength: number
}

type Shape = Circle | Square

const calculateArea = (shape: Shape) => {
    if (shape.kind === 'circle') {
        console.log('Circle radius: ', shape.radius)
    } else {
        console.log('Square length: ', shape.sideLength)
    }
}
        

I have 2 interfaces Circle and Square. Both of them have one common property which is kind. I am also using a unique value for this property. Inside the Circle interface, the kind property has the value "circle" and inside the Square interface, it has the value "square".

The calculateArea() function accepts a parameter that can be either a Circle or a Square type. You can access the kind property from this shape parameter because both interfaces have this property.

If it has the value circle, then we know it's a Circle and can safely access the radius property. Otherwise, it must be a Square and we can safely access the sideLength property.

          type Shape = 'circle' | 'square'

const calculateArea = (shape: Shape) => {
    if (shape === 'circle') {
        console.log("It's a circle")
    } else {
        console.log("It's a square")
    }
}
        

You can also use this technique without objects. Here I have a union type with some values. Now You can perform some logic based on the value.

Also Read: Mastering Type Casting in TypeScript: A Complete Guide

Custom User-Defined Type Guard

Custom user-defined type guards in TypeScript allow developers to define their own type guards with specific logic. These type guards are defined as functions that accept a value with an unknown type and return a boolean.

In this example, I have two interfaces, Person and Company, that have a common property name. I am defining a custom type guard function isCompany that checks if a given object has a property industry or not, and returns a boolean.

          interface Person {
    name: string
}

interface Company {
    name: string
    industry: string
}

const isCompany = (obj: Person | Company): obj is Company => {
    return (obj as Company).industry !== undefined
}

const printDetails = (obj: Person | Company) => {
    console.log(`Name: ${obj.name}`)

    if (isCompany(obj)) {
        console.log(`Industry: ${obj.industry}`)
    }
}

const person: Person = { name: 'Alice' }
const company: Company = { name: 'ABC Ltd.', industry: 'Technology' }

printDetails(person)
// Name: Alice

printDetails(company)
// Name: ABC Ltd.
// Industry: Technology
        

This isCompany function takes an object with a union type Person | Company. Inside the function, I check if the given object has a property industry by using the !== operator. If the property exists, we return true, otherwise false.

In the printDetails function, I take an object of type Person | Company as an argument. We use the isCompany type guard function to narrow the type of this object. If isCompany returns true, we can safely access the industry property.

When Should You Use Type Guards in TypeScript?

Type guards in TypeScript are useful when you need to narrow down the type of a variable at runtime. They help ensure that your code is type-safe and reduce the risk of runtime errors.

You should use type guards in TypeScript in the following scenarios:

  • When dealing with union types: Union types can be problematic since TypeScript may not know the exact type of the variable at runtime. Type guards can help narrow down the type of the variable based on a certain condition.
  • When working with third-party libraries: You may need to use type guards to check if the values returned by a third-party library are of the expected type.
  • When validating user input: Type guards can be useful for validating user input to ensure that the input is of the expected type.
  • When working with complex data structures: Type guards can help you avoid errors when working with complex data structures, such as objects with nested properties or arrays of objects.

Benefits of Using TypeScript Type Guards

Using type guards in TypeScript can provide several benefits for type safety and code quality:

  • Improved type safety: Type guards help ensure that variables are of the expected type, reducing the risk of runtime errors caused by unexpected types. This makes your code more robust and reliable.
  • Better code quality: Type guards make your code more readable and self-documenting by providing more information about the types of variables. This can help other developers understand your code more easily, leading to better code quality and maintainability.
  • Reduced debugging time: By catching type errors at compile time instead of runtime, type guards can reduce the time and effort required to debug your code. This can save you and your team valuable time and resources.
  • Better developer experience: Using type guards can help you catch errors early in the development process, leading to a smoother and more enjoyable development experience. This can also help improve your overall productivity.

Also Read: TypeScript Type vs. Interface: Which One Should You Use?

Conclusion

Mastering type guards in TypeScript can significantly improve the type safety and code quality of your projects. They allow you to narrow down the type of a variable based on its runtime value, making it easier to write type-safe code.

I have discussed several ways to implement type guards in your TypeScript project. Most of the time we use built-in guards like typeof, in operator, instanceof operator, etc.

But if you have any specific need in a project, you are allowed to write your own type guard functions as well. Each approach has its advantages and limitations, and it's important to choose the right one for the specific use case.

Related Posts