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 Background Title Type Guard - Type Narrowing Flow Left: Union Type Original Union Type string | number | boolean Arrow 1 typeof Middle: typeof string typeof === "string" Narrowed to string Arrow 2 Branch Right: Result Type determined Can access .length Bottom: Type Guard Types Type Guard Methods Method 1 typeof typeof x === "string" Primitive type Method 2 instanceof x instanceof Array Class instance Method 3 Custom guard x is String value is Type Method 4 in "prop" in x Property check Arrow markers

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

// Define a function that receives a union type
// 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

// Define a Dog class
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

// Define a custom type guard function
// 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 typevalue 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

// Define two interfaces with different properties
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

// Define the Circle interface, using the kind property as the discriminant
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

// Define a function parameter that may be null
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

// A string that may be undefined
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 bevalue 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:Throughvalue 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.
Other Extensions