If you're looking to make your code safer through type narrowing, TypeScript offers several helpful features. One of these is type guards, which come built into the language and can also be custom-defined for specific use cases. Built-in type guards in TypeScript - including typeof, instanceof, Array.isArray(), and the in operator - come ready-made for general use. Meanwhile, custom type guards are designed to cater to specific situations in your code. Both are commonly used in parts of code with conditional statements, helping to refine types in your code smoothly and effectively.
In this topic, you'll learn how to use built-in type guards in TypeScript, understanding their general functions and when to use them.
'typeof': The primitive type inspector
In TypeScript, the typeof operator determines the type of a value. When used with an operand (the value you're checking), it returns a string detailing what type the operand is. It's effective for checking types like string, number, boolean, undefined, function, and symbol. However, its use with non-primitive types, like objects, is limited because typeof will return an "object" for most non-primitive types, including arrays. This makes it less useful for distinguishing between various object types. For example, both an array and a simple object will return "object" when checked with typeof.
Let's explore an example to understand the behavior of the typeof operator with both primitive and non-primitive types in TypeScript.
let greeting: string = "Hello, TypeScript!";
let luckyNumber: number = 42;
let isSunny: boolean = true;
let mystery: undefined;
let performMagic: Function = () => {};
console.log(typeof greeting); // Outputs: "string"
console.log(typeof luckyNumber); // Outputs: "number"
console.log(typeof isSunny); // Outputs: "boolean"
console.log(typeof mystery); // Outputs: "undefined"
console.log(typeof performMagic); // Outputs: "function"
let treasureMap = { location: "Hidden Island" };
let fibonacci: number[] = [1, 1, 2, 3, 5];
console.log(typeof treasureMap); // Outputs: "object"
console.log(typeof fibonacci); // Outputs: "object"In this snippet, typeof checks the types of various variables. It accurately identifies primitives, like strings, numbers, and booleans, and returns their type as a string. But for non-primitives like the object treasureMap and the array fibonacci, it simply returns "object" for both. This demonstrates typeof's limitation with non-primitive types: it can't differentiate between different types of objects. For more specific needs like distinguishing arrays from other objects, we'll need to use other methods or type guards.
Let's also check the following code where the typeof operator is used as a type guard in TypeScript:
function processInput(input: string | number) {
if (typeof input === "string") {
// TypeScript recognises 'input' as a string in this block
console.log("Input is a string:", input.toUpperCase());
} else {
// TypeScript knows 'input' is a number here
console.log("Input is a number:", input.toFixed(2));
}
}In this example, input can be either a string or a number. We used typeof to check the type of input. If input is a string, TypeScript narrows its type to string within the first block, then we can use string methods like toUpperCase(). If it's not a string, then it must be a number, and we can use number methods like toFixed().
Using the typeof operator as a type guard in this manner ensures that we're using input correctly, especially for primitives, which in turn makes our code more dependable and less prone to errors.
'instanceof': Class detector
instanceof is a binary operator (meaning it operates on two operands) used to test whether an object is an instance of a particular class or constructor function. It returns a boolean value: true if the object is an instance of the specified class, and false if it's not. This can be particularly helpful when dealing with custom classes or when you need to differentiate between different types of objects.
For instance, let's say we have a base class Vehicle and two subclasses: Car and Motorcycle. We can use instanceof to find out which specific type of vehicle we're dealing with:
class Vehicle {
startEngine(): string {
return "Engine started";
}
}
class Car extends Vehicle {
openTrunk(): string {
return "Trunk opened";
}
}
class Motorcycle extends Vehicle {
putOnHelmet(): string {
return "Helmet on";
}
}
let myVehicle = new Car();
console.log(myVehicle instanceof Vehicle); // Outputs: true
console.log(myVehicle instanceof Car); // Outputs: true
console.log(myVehicle instanceof Motorcycle);// Outputs: falseIn this example, myVehicle is an instance of Car, which is a subclass of Vehicle. Therefore, checking if myVehicle is an instance of Vehicle or Car will return true. However, as myVehicle isn't an instance of Motorcycle, checking if it is will return false.
The instanceof operator comes in handy when dealing with different types of objects linked by a common base class, such as different types of vehicles in this scenario. With instanceof, we can execute specific actions for each type, such as opening a car's trunk or wearing a motorcycle helmet.
'Array.isArray()': Array identifier
In TypeScript, and by extension in JavaScript, arrays are viewed as a unique type of object. As a result, when you use the typeof operator on an array, it returns "object" instead of "array", which can be misleading when you're trying to determine whether a value is an array or not.
For that reason, TypeScript and JavaScript provide a built-in method called Array.isArray(). This method checks to see if a value is an array and returns a boolean: true if the value is an array, and false if it's not.
While typeof can't reliably identify arrays, TypeScript offers the Array.isArray() method specifically to verify if a value is an array. It's important because, in JavaScript, arrays are actually categorized as objects, and using typeof will return "object" for them.
Here's the example explained:
let numbers = [1, 2, 3];
let name = "TypeScript";
console.log(Array.isArray(numbers)); // Outputs: true
console.log(Array.isArray(name)); // Outputs: falseIn this case, Array.isArray(numbers) returns true because numbers is indeed an array. On the other hand, Array.isArray(name) returns false because name is a string, not an array.
The Array.isArray() method turns out to be quite useful when you're working with data that could be of various types. For example, when writing a function that accepts both arrays and other data types, you might need to use Array.isArray() to correctly handle array data.
'in' operator: Property checker
The in operator in TypeScript is another built-in type guard that lets us check if a specific property exists within an object. If the property is present (either because it owns it or because it inherited it), the in operator will return true. If not, it will return false. This feature can be particularly useful for type narrowing where you have different types of objects with unique properties.
To understand how it works, consider the following code snippet:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
// TypeScript knows 'animal' is a Fish here
animal.swim();
} else {
// TypeScript knows 'animal' is a Bird here
animal.fly();
}
}In this example, Fish and Bird are distinct types that carry unique methods. The move function accepts an object that could belong to either type. Inside the function, the in operator checks if the swim property exists in the object. If it does, TypeScript concludes that the object is of the Fish type, and it's safe to call animal.swim(). If not, the object must be a Bird, so it's safe to call animal.fly(). This demonstrates how the in operator helps TypeScript ascertain the exact type of an object, ensuring type safety within the function. This ultimately makes your code more reliable and less likely to run into errors.
Conclusion
TypeScript's type narrowing techniques are versatile and diverse. Using built-in type guards like typeof, instanceof, Array.isArray(), and the in operator makes your code more secure and predictable. Each guard plays a unique role: typeof verifies primitive types, instanceof examines class instances, Array.isArray() validates arrays, and in looks for properties. By understanding and applying these tools, you can make the most of TypeScript's type system; this leads to fewer errors and enhanced code quality in your TypeScript applications.