MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

使用TypeScript类型守卫处理联合类型

2024-01-165.5k 阅读

理解联合类型

在 TypeScript 中,联合类型是一种非常有用的类型定义方式,它允许一个变量具有多种类型。例如,我们可能有一个函数,它既可以接受字符串,也可以接受数字:

function printValue(value: string | number) {
    console.log(value);
}

printValue('Hello');
printValue(42);

在上述代码中,printValue 函数的参数 value 是一个联合类型 string | number,这意味着它可以接受字符串类型的值,也可以接受数字类型的值。

然而,当我们需要对联合类型的值进行特定类型的操作时,就会遇到一些挑战。比如,我们想要对 value 进行字符串拼接或者数字加法操作:

function printValue(value: string | number) {
    if (typeof value ==='string') {
        console.log(value + ' World');
    } else {
        console.log(value + 1);
    }
}

printValue('Hello');
printValue(42);

这里我们使用了 typeof 操作符来判断 value 的实际类型,然后根据不同类型进行相应的操作。这其实就是类型守卫的一种简单应用。

类型守卫的概念

类型守卫是一种运行时检查机制,它可以在代码执行过程中缩小类型的范围。类型守卫的返回值是一个类型谓词,形式为 parameterName is Type,其中 parameterName 是正在被检查的参数名,Type 是要判断的类型。

TypeScript 内置了一些类型守卫,比如 typeofinstanceof。我们来看 instanceof 的例子,假设我们有一个类继承体系:

class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}

class Dog extends Animal {
    bark() {
        console.log('Woof!');
    }
}

class Cat extends Animal {
    meow() {
        console.log('Meow!');
    }
}

function handleAnimal(animal: Animal) {
    if (animal instanceof Dog) {
        animal.bark();
    } else if (animal instanceof Cat) {
        animal.meow();
    }
}

const myDog = new Dog('Buddy');
const myCat = new Cat('Whiskers');

handleAnimal(myDog);
handleAnimal(myCat);

handleAnimal 函数中,我们使用 instanceof 类型守卫来判断 animal 实际是 Dog 还是 Cat 类型,然后调用相应的方法。

用户自定义类型守卫

除了内置的类型守卫,我们还可以定义自己的类型守卫。自定义类型守卫函数通常会接受一个联合类型的参数,并返回一个类型谓词。

假设我们有一个联合类型 string | number,我们想要定义一个类型守卫来判断一个值是否为字符串:

function isString(value: string | number): value is string {
    return typeof value ==='string';
}

function printValue(value: string | number) {
    if (isString(value)) {
        console.log(value + ' World');
    } else {
        console.log(value + 1);
    }
}

printValue('Hello');
printValue(42);

在上述代码中,isString 函数就是一个自定义类型守卫。它接受 string | number 类型的 value,返回 value is string,表示如果返回 true,则 valuestring 类型。

在函数重载中使用类型守卫

函数重载在 TypeScript 中允许我们定义多个同名但参数列表不同的函数。结合类型守卫,我们可以更灵活地处理不同类型的输入。

考虑一个 add 函数,它既可以接受两个数字相加,也可以接受两个字符串拼接:

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string | number, b: string | number): string | number {
    if (typeof a === 'number' && typeof b === 'number') {
        return a + b;
    } else if (typeof a ==='string' && typeof b ==='string') {
        return a + b;
    }
    throw new Error('Invalid types for add');
}

const result1 = add(1, 2);
const result2 = add('Hello ', 'World');

在这个例子中,我们首先定义了两个函数重载签名,分别处理数字相加和字符串拼接。然后在实现函数中,使用 typeof 类型守卫来判断输入的类型,并执行相应的操作。

类型守卫与类型别名

类型别名是给类型定义一个新名字的方式。当联合类型使用类型别名定义时,类型守卫同样适用。

type StringOrNumber = string | number;

function isString(value: StringOrNumber): value is string {
    return typeof value ==='string';
}

function printValue(value: StringOrNumber) {
    if (isString(value)) {
        console.log(value + ' World');
    } else {
        console.log(value + 1);
    }
}

printValue('Hello');
printValue(42);

这里我们使用 type 关键字定义了 StringOrNumber 类型别名,它等价于 string | number。然后我们可以像处理普通联合类型一样,使用自定义类型守卫 isString 来处理这个类型别名。

类型守卫与交叉类型

交叉类型是将多个类型合并为一个类型,它要求一个值必须同时满足多个类型的要求。虽然交叉类型和联合类型不同,但类型守卫在处理涉及交叉类型的联合类型时也很有用。

interface HasLength {
    length: number;
}

interface IsNumber {
    value: number;
}

type LengthOrNumber = HasLength | IsNumber;

function handleValue(value: LengthOrNumber) {
    if ('length' in value) {
        console.log('Length is', value.length);
    } else if ('value' in value) {
        console.log('Value is', value.value);
    }
}

const obj1: HasLength = { length: 5 };
const obj2: IsNumber = { value: 10 };

handleValue(obj1);
handleValue(obj2);

在上述代码中,我们定义了 HasLengthIsNumber 两个接口,并通过 type 创建了联合类型 LengthOrNumber。在 handleValue 函数中,我们使用 in 操作符作为类型守卫,判断 value 实际属于哪个类型,从而执行相应的操作。

类型守卫在数组联合类型中的应用

当数组元素是联合类型时,我们同样可以使用类型守卫来处理。

function printArrayValues(arr: (string | number)[]) {
    arr.forEach((value) => {
        if (typeof value ==='string') {
            console.log(value + ' World');
        } else {
            console.log(value + 1);
        }
    });
}

const myArray: (string | number)[] = ['Hello', 42];
printArrayValues(myArray);

在这个例子中,myArray 是一个元素为 string | number 联合类型的数组。在 printArrayValues 函数中,我们通过 forEach 遍历数组元素,并使用 typeof 类型守卫对每个元素进行类型判断和相应操作。

高级类型守卫技巧

  1. 联合类型的反向类型守卫:有时候我们需要判断一个值不是某个类型,例如判断一个值不是字符串:
function isNotString(value: string | number): value is number {
    return typeof value!=='string';
}

function printValue(value: string | number) {
    if (isNotString(value)) {
        console.log(value + 1);
    } else {
        console.log(value + ' World');
    }
}

printValue('Hello');
printValue(42);

这里 isNotString 就是一个反向类型守卫,它判断 value 不是字符串类型,即 value 是数字类型。

  1. 结合多个类型守卫:在复杂的场景中,我们可能需要结合多个类型守卫来更精确地判断类型。
class Shape {}
class Circle extends Shape {
    radius: number;
    constructor(radius: number) {
        super();
        this.radius = radius;
    }
}
class Square extends Shape {
    side: number;
    constructor(side: number) {
        super();
        this.side = side;
    }
}

function draw(shape: Shape | Circle | Square) {
    if (shape instanceof Circle) {
        console.log('Drawing a circle with radius', shape.radius);
    } else if (shape instanceof Square) {
        console.log('Drawing a square with side', shape.side);
    } else if ('radius' in shape) {
        console.log('This should not happen, but it is like a circle with radius', (shape as Circle).radius);
    } else if ('side' in shape) {
        console.log('This should not happen, but it is like a square with side', (shape as Square).side);
    }
}

const circle = new Circle(5);
const square = new Square(4);
const shape: Shape = new Shape();

draw(circle);
draw(square);
draw(shape);

draw 函数中,我们首先使用 instanceof 类型守卫判断 shapeCircle 还是 Square。然后又结合 in 操作符作为额外的类型守卫,处理一些特殊情况(虽然这里的特殊情况逻辑可能在实际中并不常见,但展示了结合多个类型守卫的方法)。

类型守卫的性能考虑

虽然类型守卫在处理联合类型时非常有用,但在性能敏感的应用中,我们需要注意它们的使用。例如,过多的 typeof 或者 instanceof 判断可能会带来一定的性能开销,尤其是在循环中频繁使用时。

function processValues(arr: (string | number)[]) {
    for (let i = 0; i < arr.length; i++) {
        if (typeof arr[i] ==='string') {
            // 执行字符串相关操作
        } else {
            // 执行数字相关操作
        }
    }
}

const largeArray: (string | number)[] = [];
for (let i = 0; i < 1000000; i++) {
    if (i % 2 === 0) {
        largeArray.push(i);
    } else {
        largeArray.push('' + i);
    }
}

processValues(largeArray);

在上述代码中,processValues 函数对一个包含大量元素的联合类型数组进行遍历,并使用 typeof 类型守卫。如果这种操作非常频繁,可能会影响性能。在这种情况下,我们可以考虑优化算法,减少类型判断的次数,或者在设计阶段尽量避免频繁处理这种复杂的联合类型。

类型守卫与类型推断

类型守卫不仅可以在运行时缩小类型范围,还能帮助 TypeScript 进行更好的类型推断。

function getValue(): string | number {
    return Math.random() > 0.5? 'Hello' : 42;
}

const value = getValue();
if (typeof value ==='string') {
    // 这里 TypeScript 知道 value 是 string 类型
    console.log(value.length);
} else {
    // 这里 TypeScript 知道 value 是 number 类型
    console.log(value.toFixed(2));
}

在上述代码中,getValue 函数返回一个 string | number 联合类型的值。通过 typeof 类型守卫,TypeScript 能够在不同分支中准确推断出 value 的类型,从而允许我们使用相应类型的属性和方法。

类型守卫在接口和类继承体系中的应用

在接口和类继承体系中,类型守卫可以帮助我们处理不同类型的对象。

interface Shape {
    draw(): void;
}

class Circle implements Shape {
    radius: number;
    constructor(radius: number) {
        this.radius = radius;
    }
    draw() {
        console.log('Drawing a circle with radius', this.radius);
    }
}

class Square implements Shape {
    side: number;
    constructor(side: number) {
        this.side = side;
    }
    draw() {
        console.log('Drawing a square with side', this.side);
    }
}

function drawShapes(shapes: Shape[]) {
    shapes.forEach((shape) => {
        if (shape instanceof Circle) {
            // 这里 shape 被推断为 Circle 类型
            shape.draw();
        } else if (shape instanceof Square) {
            // 这里 shape 被推断为 Square 类型
            shape.draw();
        }
    });
}

const circle = new Circle(5);
const square = new Square(4);
const shapeArray: Shape[] = [circle, square];

drawShapes(shapeArray);

在这个例子中,我们有一个 Shape 接口以及实现它的 CircleSquare 类。drawShapes 函数接受一个 Shape 数组,通过 instanceof 类型守卫,在不同分支中 shape 被准确推断为 CircleSquare 类型,从而可以调用相应的 draw 方法。

类型守卫与泛型

泛型在 TypeScript 中提供了一种创建可复用组件的方式,类型守卫在泛型代码中同样有重要作用。

function identity<T>(arg: T): T {
    return arg;
}

function printIdentity<T>(arg: T) {
    if (Array.isArray(arg)) {
        console.log('It is an array with length', arg.length);
    } else {
        console.log('It is not an array');
    }
    return identity(arg);
}

const result1 = printIdentity([1, 2, 3]);
const result2 = printIdentity('Hello');

在上述代码中,printIdentity 函数使用了泛型 T。通过 Array.isArray 类型守卫,我们可以判断传入的泛型参数是否为数组类型,并进行相应的操作。这展示了类型守卫在泛型函数中的应用,使得泛型代码可以根据实际类型进行不同的处理。

类型守卫在函数参数默认值中的应用

当函数参数有默认值且为联合类型时,类型守卫可以帮助我们正确处理默认值的类型。

function greet(name: string | undefined = 'Guest') {
    if (typeof name ==='string') {
        console.log('Hello,', name);
    } else {
        console.log('Hello, Guest');
    }
}

greet();
greet('John');

greet 函数中,参数 namestring | undefined 联合类型且有默认值 'Guest'。通过 typeof 类型守卫,我们可以确保在使用 name 时,它是 string 类型,从而避免潜在的运行时错误。

类型守卫与可选链操作符

可选链操作符 ?. 是 TypeScript 中的一个强大特性,它可以在对象属性可能为 nullundefined 时安全地访问属性。结合类型守卫,我们可以更灵活地处理复杂的对象结构。

interface User {
    profile: {
        address: {
            city: string;
        };
    };
}

function printCity(user: User | null | undefined) {
    if (user && 'profile' in user) {
        const city = user.profile?.address?.city;
        if (city) {
            console.log('City is', city);
        }
    }
}

const user1: User = {
    profile: {
        address: {
            city: 'New York'
        }
    }
};

const user2: null = null;

printCity(user1);
printCity(user2);

printCity 函数中,我们首先使用 in 类型守卫判断 user 是否存在且有 profile 属性。然后使用可选链操作符安全地获取 city 属性,避免了潜在的 nullundefined 引用错误。

类型守卫在模块导入导出中的应用

在模块中,当导入或导出联合类型的值时,类型守卫同样可以发挥作用。

// utils.ts
export type StringOrNumber = string | number;

export function isString(value: StringOrNumber): value is string {
    return typeof value ==='string';
}

// main.ts
import { StringOrNumber, isString } from './utils';

function printValue(value: StringOrNumber) {
    if (isString(value)) {
        console.log(value + ' World');
    } else {
        console.log(value + 1);
    }
}

const myValue: StringOrNumber = 42;
printValue(myValue);

在这个例子中,我们在 utils.ts 模块中定义了 StringOrNumber 联合类型和 isString 类型守卫,并在 main.ts 模块中导入使用。这展示了类型守卫在模块间传递和处理联合类型的方式。

通过以上对 TypeScript 中使用类型守卫处理联合类型的详细介绍,我们深入了解了类型守卫的概念、应用场景以及与其他 TypeScript 特性的结合使用。在实际开发中,合理运用类型守卫可以提高代码的健壮性和可维护性,减少运行时错误。无论是简单的联合类型,还是涉及接口、类、泛型等复杂结构的联合类型,类型守卫都为我们提供了有效的类型处理手段。