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.