TypeScript Type Guards
Type Guards are a very important type narrowing mechanism in TypeScript.
They allow developers to use specific runtime condition checks so that the TypeScript compiler can accurately infer the specific type of a variable.
With type guards, we can safely access properties and methods of a specific type within a union type variable.
Why Do We Need Type Guards?
In TypeScript, a variable may be declared as a union of multiple types.
When we need to perform different operations based on the specific type, the compiler cannot automatically determine the current concrete type.
Type guards are the key mechanism for solving this problem.
Concept Explanation:The core principle of type guards is "type narrowing". Through conditional checks, TypeScript automatically narrows a union type down to a specific type.
SVG Diagram: Type Guard Flow
typeof Type Guard
typeof is the most commonly used type guard, used to check primitive types (string, number, boolean, etc.).
It returns a string indicating the type of the value.
typeof Basic Usage
// The parameter value could be a string or a number
function printValue(value: string | number): void {
// Use typeof to check the type
// When the if condition is true, TypeScript automatically narrows value to string type
if (typeof value === "string") {
// At this point TypeScript knows value is a string
// Can safely access the length property of the string
console.log("String length: " + value.length);
} else {
// In the else branch, TypeScript knows value is not a string
// It can only be of type number
// Can safely perform mathematical operations
console.log("Doubled number: " + (value * 2));
}
}
// Test calls
printValue("hello"); // Pass a string
printValue(42); // Pass a number
Output:
字符串长度: 5 数字翻倍: 84
Types supported by typeof:
- "string"- string type
- "number"- number type (including NaN and Infinity)
- "boolean"- boolean type
- "undefined"- undefined type
- "object"- object type (note: arrays and null are also recognized as "object")
- "function"- function type
Note:typeof returns "object" for both arrays and null. If you need to precisely distinguish arrays from objects, you need to use other methods.
instanceof Type Guard
instanceof is used to check whether an object is an instance of a certain class.
It determines the type by checking the object's prototype chain.
instanceof Basic Usage
class Dog {
// The dog's bark method
bark(): void {
console.log("Woof woof woof!");
}
}
// Define a Cat class
class Cat {
// The cat's meow method
meow(): void {
console.log("Meow meow meow!");
}
}
// Function that receives a union type
function makeSound(animal: Dog | Cat): void {
// Use instanceof to check whether animal is a Dog or a Cat
// When the if condition is true, TypeScript narrows animal to Dog type
if (animal instanceof Dog) {
// At this point, you can call Dog-specific methods
animal.bark();
} else {
// In the else branch, TypeScript narrows animal to Cat type
animal.meow();
}
}
// Test calls
makeSound(new Dog()); // Create a Dog instance and call it
makeSound(new Cat()); // Create a Cat instance and call it
Output:
Woof woof woof! Meow meow meow!
Explanation:
instanceof checks the object's prototype chain, so it can only be used with class instances, not with interfaces or type aliases.
Custom Type Guards
When the built-in typeof and instanceof do not meet the requirements, you can create custom type guard functions.
Custom type guards usevalue is Typethe return type syntax.
Custom Guard Functions
// The return type uses the "value is Type" format
// This tells TypeScript: when the function returns true, the parameter type is string
function isString(value: any): value is string {
// Use typeof to check if it is a string
return typeof value === "string";
}
// Another custom guard: check if it is a number
function isNumber(value: any): value is number {
return typeof value === "number";
}
// Define an array type guard
function isArray(value: any): value is any[] {
return Array.isArray(value);
}
// A function that processes values
function processValue(value: string | number | any[]): void {
// Use custom guards for type checking
if (isString(value)) {
// TypeScript knows value is of type string
// Can call the toUpperCase() method
console.log("Uppercase string: " + value.toUpperCase());
} else if (isNumber(value)) {
// TypeScript knows value is of type number
// Can call the toFixed() method
console.log("Formatted number: " + value.toFixed(2));
} else if (isArray(value)) {
// TypeScript knows value is of type array
console.log("Array length: " + value.length);
}
}
// Test calls
processValue("hello");
processValue(3.14159);
processValue([1, 2, 3, 4, 5]);
Output:
字符串转大写: HELLO 数字格式化: 3.14 数组长度: 5
Tip:The key to a custom type guard is the return type
value is Type, which is the marker TypeScript uses to identify a type guard.
in Operator Type Guard
The in operator can check whether an object contains a certain property.
Using in in conditional checks, TypeScript automatically narrows the object's type scope.
in Operator Usage
interface A {
a: string; // Only has property a
}
interface B {
b: number; // Only has property b
}
// Function that receives a union type
function process(obj: A | B): void {
// Use in to check whether the object contains property "a"
if ("a" in obj) {
// In the if branch, TypeScript knows obj contains property a
// Therefore obj's type is narrowed to A
console.log("A's property a: " + obj.a);
} else {
// In the else branch, obj does not contain property a
// TypeScript knows obj can only be of type B
// Therefore you can safely access property b
console.log("B's property b: " + obj.b);
}
}
// Test calls
process({ a: "hello" }); // Pass in an object containing property a
process({ b: 42 }); // Pass in an object containing property b
Running result:
A 的属性 a: hello B 的属性 b: 42
Discriminated Unions and Type Guards
A discriminated union is a powerful pattern that distinguishes union type members through a common "discriminant" property.
Combined with switch statements or if checks, complete type guards can be implemented.
Implementing a Calculator with Discriminated Unions
interface Circle {
kind: "circle"; // Discriminant field: value is "circle"
radius: number; // radius
}
// Define the Rectangle interface
interface Rectangle {
kind: "rectangle"; // Discriminant field: value is "rectangle"
width: number; // width
height: number; // height
}
// Define the Triangle interface
interface Triangle {
kind: "triangle"; // Discriminant field: value is "triangle"
base: number; // base
height: number; // height
}
// Define the union type
type Shape = Circle | Rectangle | Triangle;
// Function to calculate area
function getArea(shape: Shape): number {
// Use a switch statement for type guarding
// Based on the value of the kind property, TypeScript automatically narrows the type
switch (shape.kind) {
case "circle":
// shape is narrowed to the Circle type
// Can access the radius property
return Math.PI * shape.radius ** 2;
case "rectangle":
// shape is narrowed to the Rectangle type
// Can access the width and height properties
return shape.width * shape.height;
case "triangle":
// shape is narrowed to the Triangle type
return 0.5 * shape.base * shape.height;
}
}
// Test calls
var circle = { kind: "circle" as const, radius: 5 };
var rectangle = { kind: "rectangle" as const, width: 4, height: 6 };
var triangle = { kind: "triangle" as const, base: 3, height: 4 };
console.log("Circle area: " + getArea(circle).toFixed(2));
console.log("Rectangle area: " + getArea(rectangle));
console.log("Triangle area: " + getArea(triangle));
Running result:
圆形面积: 78.54 矩形面积: 24 三角形面积: 6
Note:Discriminated unions are one of the most recommended patterns in TypeScript. They distinguish different type members through a common literal property (usually kind or type), making the code both type-safe and easy to maintain.
null and undefined Checks
When dealing with values that may be null or undefined, direct equality checks are also effective type guards.
null Check
function getLength(str: string | null): number {
// Directly check that str is not equal to null
// When the condition is true, TypeScript knows str is not null
// At this point, str's properties can be safely accessed
if (str !== null) {
return str.length;
}
// Handling the null case
return 0;
}
// Test calls
console.log(getLength("hello")); // Normal string
console.log(getLength(null)); // Pass in null
Running result:
5 0
Tip:After enabling strictNullChecks, it is recommended to always perform null checks. Optional chaining (?.) and nullish coalescing (??) can be used to simplify the code.
Truthiness Narrowing
In addition to explicit type checks, TypeScript also narrows type ranges through truthiness assertions.
Truthiness Narrowing
function greet(name?: string): string {
// Use the short-circuit operator: if name is undefined or an empty string, use a default value
// In the code block after &&, TypeScript knows name must have a value
return name && "Hello, " + name;
}
// Test
console.log(greet("EXAMPLE"));
console.log(greet());
Running result:
Hello, EXAMPLE Hello, undefined
Important Notes
- Type guards must be used in conditional branches:Only after using a type guard for a conditional check will TypeScript perform type narrowing.
- The return type must be a type predicate:The return type of a custom type guard must be
value is Typeformat - Discriminated unions are the best practice:For complex union types, it is recommended to use the discriminated union pattern.
- Note the completeness of type narrowing:When using switch statements, it is recommended to handle all possible branches.
Recommendation:When dealing with union types, give priority to the discriminated union pattern. It not only makes the code clearer but also fully leverages TypeScript's type inference capabilities.
Summary
Type guards are an important part of TypeScript's type system.
- typeof:The most common way, suitable for checking primitive types.
- instanceof:Check whether an object is an instance of a specific class.
- Custom guards:Through
value is Typethe syntax, flexible type checks can be implemented. - in:Check whether an object contains a specific property.
- Discriminated unions:Recommended best practice pattern, distinguishing types through discriminant fields.
- Truthiness narrowing:Leverage JavaScript's truthiness checks to narrow types.