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.