阿里云主机折上折
  • 微信号
Current Site:Index > Type guards and type narrowing

Type guards and type narrowing

Author:Chuan Chen 阅读数:39374人阅读 分类: TypeScript

Type Guards and Type Narrowing Concepts

TypeScript's type system allows developers to more precisely control the type scope of variables through type guards and type narrowing. A type guard is a runtime check used to determine the specific type of a variable, while type narrowing is the TypeScript compiler automatically reducing the type scope of a variable to a more specific type based on these checks. These two concepts are closely related and work together to improve type safety in code.

typeof Type Guards

typeof is one of the most basic type guards. It narrows the TypeScript type scope by checking the JavaScript type of a variable. When the typeof operator is used on a variable, TypeScript can recognize specific type patterns and adjust type inference accordingly.

function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

In this example, typeof padding === "number" lets TypeScript know that within the first conditional block, padding must be of type number, allowing safe numerical operations.

instanceof Type Guards

For custom classes or built-in objects, the instanceof operator is a more suitable type guard. It checks whether the object's prototype chain contains the prototype property of a specific constructor.

class Bird {
    fly() {
        console.log("flying");
    }
}

class Fish {
    swim() {
        console.log("swimming");
    }
}

function move(pet: Bird | Fish) {
    if (pet instanceof Bird) {
        pet.fly();  // Here, `pet` is narrowed to type `Bird`
    } else {
        pet.swim(); // Here, `pet` is narrowed to type `Fish`
    }
}

Custom Type Guards

When built-in type guards are insufficient, you can define your own type predicate functions. A custom type guard is a function that returns a type predicate in the form parameterName is Type.

interface Cat {
    meow(): void;
}

interface Dog {
    bark(): void;
}

function isCat(animal: Cat | Dog): animal is Cat {
    return (animal as Cat).meow !== undefined;
}

function animalSound(animal: Cat | Dog) {
    if (isCat(animal)) {
        animal.meow();  // Type narrowed to `Cat`
    } else {
        animal.bark();  // Type narrowed to `Dog`
    }
}

Discriminated Unions

Discriminated unions are a powerful tool for type narrowing. They require each member of the union type to have a common singleton type property (i.e., a "tag").

type Shape = 
    | { kind: "circle"; radius: number }
    | { kind: "square"; sideLength: number }
    | { kind: "triangle"; base: number; height: number };

function getArea(shape: Shape): number {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius ** 2;  // `shape` narrowed to circle
        case "square":
            return shape.sideLength ** 2;       // `shape` narrowed to square
        case "triangle":
            return (shape.base * shape.height) / 2; // `shape` narrowed to triangle
    }
}

Truthiness Narrowing

In JavaScript, conditional checks automatically convert values to booleans. TypeScript leverages this for type narrowing, especially when dealing with values that might be null or undefined.

function printAll(strs: string | string[] | null) {
    if (strs && typeof strs === "object") {
        for (const s of strs) {  // `strs` narrowed to `string[]`
            console.log(s);
        }
    } else if (typeof strs === "string") {
        console.log(strs);  // `strs` narrowed to `string`
    }
}

Equality Narrowing

TypeScript uses equality checks like ===, !==, ==, and != to narrow types. This is particularly useful when working with literal types.

function example(x: string | number, y: string | boolean) {
    if (x === y) {
        // Here, both `x` and `y` are narrowed to `string`
        x.toUpperCase();
        y.toLowerCase();
    } else {
        console.log(x);  // `x`: string | number
        console.log(y);  // `y`: string | boolean
    }
}

in Operator Narrowing

The in operator checks whether an object has a specific property, and TypeScript uses this for type narrowing.

type Admin = {
    name: string;
    privileges: string[];
};

type Employee = {
    name: string;
    startDate: Date;
};

function printInformation(emp: Admin | Employee) {
    console.log("Name: " + emp.name);
    if ("privileges" in emp) {
        console.log("Privileges: " + emp.privileges);  // `emp` narrowed to `Admin`
    }
    if ("startDate" in emp) {
        console.log("Start Date: " + emp.startDate);   // `emp` narrowed to `Employee`
    }
}

Type Assertions and Narrowing

In some cases, developers know more about a value's type than TypeScript does. Type assertions can be used to force type narrowing, but note that they do not perform runtime checks.

function getLength(input: string | number): number {
    if ((input as string).length) {
        return (input as string).length;  // Assert `input` as `string`
    } else {
        return input.toString().length;   // `input` as `number`
    }
}

Control Flow Analysis

TypeScript's control flow analysis tracks type changes of variables in code, enabling automatic type narrowing. This analysis is continuous and changes with the execution path.

function example() {
    let x: string | number | boolean;
    
    x = Math.random() < 0.5;
    console.log(x);  // `x`: boolean
    
    if (Math.random() < 0.5) {
        x = "hello";
        console.log(x);  // `x`: string
    } else {
        x = 100;
        console.log(x);  // `x`: number
    }
    
    return x;  // `x`: string | number
}

never Type and Exhaustiveness Checking

The never type represents values that should never occur. It is often used for exhaustiveness checking to ensure all possible types are handled.

type Shape = Circle | Square;

interface Circle {
    kind: "circle";
    radius: number;
}

interface Square {
    kind: "square";
    sideLength: number;
}

function getArea(shape: Shape) {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius ** 2;
        case "square":
            return shape.sideLength ** 2;
        default:
            const _exhaustiveCheck: never = shape;
            return _exhaustiveCheck;
    }
}

If a new Shape type is added later but getArea is not updated, TypeScript will report an error in the default branch because shape cannot be assigned to the never type.

Type Narrowing with Generics

Type narrowing also applies in generic functions, but special attention is needed for type parameter constraints.

function process<T extends string | number>(value: T): T {
    if (typeof value === "string") {
        // Here, `value` is narrowed to `T & string`
        return value.toUpperCase() as T;
    } else {
        // Here, `value` is narrowed to `T & number`
        return (value * 2) as T;
    }
}

Limitations of Type Narrowing

Although type narrowing is powerful, TypeScript may fail to correctly infer types in certain scenarios, especially with complex logic or asynchronous code.

function isStringArray(arr: any[]): arr is string[] {
    return arr.every(item => typeof item === "string");
}

async function fetchData() {
    const response = await fetch("/api/data");
    const data = await response.json();
    
    if (isStringArray(data)) {
        // Here, `data` should be `string[]`, but TypeScript may not fully confirm it
        return data.map(s => s.toUpperCase());
    }
    return [];
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.