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

TypeScript 高级类型:类型保护与类型推断的协同作用

2021-10-112.8k 阅读

理解 TypeScript 中的类型保护

什么是类型保护

在 TypeScript 中,类型保护是一种机制,它允许我们在特定的代码块中细化类型信息。通过使用类型保护,我们可以在运行时检查变量的类型,并根据检查结果在该代码块内使用更具体的类型。这有助于避免类型错误,提高代码的健壮性。

例如,假设我们有一个函数,它接受一个参数,这个参数可能是字符串或者数字。我们想要在函数内部根据参数的实际类型执行不同的操作。在没有类型保护的情况下,我们只能将参数当作联合类型(string | number)来处理,这限制了我们可以对该参数执行的操作。

function printValue(value: string | number) {
    // 这里 value 的类型是 string | number,不能直接调用字符串或数字特有的方法
    // 比如 value.length 或 value.toFixed() 都会报错
}

而使用类型保护,我们可以在函数内部确定参数的实际类型,从而执行相应的操作。

function printValue(value: string | number) {
    if (typeof value ==='string') {
        console.log(value.length);
    } else {
        console.log(value.toFixed(2));
    }
}

在这个例子中,typeof value ==='string' 就是一个类型保护。它在运行时检查 value 的类型,如果是字符串类型,那么在 if 代码块内,TypeScript 就会认为 value 的类型是 string,从而允许我们调用 value.length

类型保护的常见形式

  1. typeof 类型保护
    • typeof 操作符是最常见的类型保护方式之一,它可以用于区分基本类型,如 stringnumberbooleanundefined 等。
    • 示例:
function handleValue(value: string | number | boolean) {
    if (typeof value ==='string') {
        console.log('It is a string:', value.toUpperCase());
    } else if (typeof value === 'number') {
        console.log('It is a number:', value.toFixed(2));
    } else {
        console.log('It is a boolean:', value? 'true' : 'false');
    }
}

在这个函数中,typeof 类型保护帮助我们根据 value 的实际类型执行不同的操作。

  1. instanceof 类型保护
    • 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();
    }
}

在这个例子中,instanceof 类型保护让我们能够在 handleAnimal 函数中根据 animal 的实际类型调用相应的方法。

  1. in 类型保护
    • in 操作符可以用来检查对象是否包含某个属性。这在处理具有可选属性的对象类型时很有用。
    • 示例:
interface UserWithEmail {
    name: string;
    email: string;
}
interface UserWithPhone {
    name: string;
    phone: string;
}
type User = UserWithEmail | UserWithPhone;
function sendMessage(user: User) {
    if ('email' in user) {
        console.log(`Sending email to ${user.email}`);
    } else {
        console.log(`Sending SMS to ${user.phone}`);
    }
}

在这个例子中,in 类型保护帮助我们根据 user 对象实际包含的属性来决定发送消息的方式。

  1. 自定义类型保护函数
    • 我们还可以定义自己的类型保护函数。一个自定义类型保护函数是一个返回 boolean 的函数,并且其参数会被 TypeScript 特殊处理,以细化参数的类型。
    • 示例:
function isString(value: any): value is string {
    return typeof value ==='string';
}
function processValue(value: string | number) {
    if (isString(value)) {
        console.log('String value:', value.length);
    } else {
        console.log('Number value:', value.toFixed(2));
    }
}

在这个例子中,isString 函数就是一个自定义类型保护函数。它的返回类型 value is string 是一个特殊的语法,告诉 TypeScript 如果函数返回 true,那么 value 的类型就是 string

类型推断与类型保护的关系

类型推断基础

类型推断是 TypeScript 的一个强大特性,它允许编译器在很多情况下自动推断出变量的类型,而不需要我们显式地声明。例如:

let num = 10; // TypeScript 推断 num 的类型为 number
let str = 'hello'; // TypeScript 推断 str 的类型为 string

当我们定义函数时,TypeScript 也能根据函数的参数和返回值推断出函数的类型。

function add(a, b) {
    return a + b;
}
// TypeScript 推断 add 函数的类型为 (a: number, b: number) => number

类型保护对类型推断的影响

  1. 局部类型细化
    • 类型保护可以在特定的代码块内细化类型推断。回到前面 printValue 的例子:
function printValue(value: string | number) {
    if (typeof value ==='string') {
        // 在这个 if 块内,TypeScript 根据类型保护推断 value 的类型为 string
        console.log(value.length);
    } else {
        // 在这个 else 块内,TypeScript 推断 value 的类型为 number
        console.log(value.toFixed(2));
    }
}

这里,类型保护 typeof value ==='string' 使得在 ifelse 块内,TypeScript 能够更精确地推断 value 的类型,从而允许我们调用相应类型的方法。

  1. 函数调用与类型推断
    • 当我们使用自定义类型保护函数时,它也会影响函数调用处的类型推断。
function isNumber(value: any): value is number {
    return typeof value === 'number';
}
function calculate(a: string | number, b: string | number) {
    if (isNumber(a) && isNumber(b)) {
        return a + b;
    }
    return null;
}
let result = calculate(10, 20);
// 由于 calculate 函数内部的类型保护,TypeScript 推断 result 的类型为 number | null

在这个例子中,isNumber 函数作为类型保护,帮助 calculate 函数内部更精确地推断 ab 的类型,进而影响了 calculate 函数返回值的类型推断,最终影响了 result 的类型推断。

类型推断对类型保护的支持

  1. 上下文类型推断
    • TypeScript 的上下文类型推断可以帮助我们更方便地编写类型保护。例如,在事件处理函数中:
document.addEventListener('click', function (event) {
    if ('target' in event) {
        let target = event.target;
        // 由于上下文类型推断,TypeScript 知道 event 是 MouseEvent 类型,所以能推断出 target 的类型
        if (typeof target === 'element') {
            target.style.color ='red';
        }
    }
});

这里,TypeScript 根据事件监听的上下文推断出 event 的类型是 MouseEvent,这使得我们在使用 in 类型保护时,能更准确地进行后续的类型相关操作。

  1. 类型推断与类型保护的协同优化
    • 类型推断和类型保护相互协同,可以优化代码的类型安全性和可读性。例如,在处理复杂的对象结构时:
interface Shape {
    kind: string;
}
interface Circle extends Shape {
    kind: 'circle';
    radius: number;
}
interface Square extends Shape {
    kind:'square';
    sideLength: number;
}
type AnyShape = Circle | Square;
function calculateArea(shape: AnyShape) {
    if (shape.kind === 'circle') {
        // 由于类型推断和类型保护的协同,TypeScript 知道 shape 是 Circle 类型
        return Math.PI * shape.radius * shape.radius;
    } else {
        // 这里 shape 被推断为 Square 类型
        return shape.sideLength * shape.sideLength;
    }
}

在这个例子中,通过类型保护 shape.kind === 'circle',结合 TypeScript 的类型推断,我们能够在函数内部准确地处理不同类型的 Shape,提高了代码的健壮性和可读性。

类型保护与类型推断在复杂场景中的应用

处理联合类型数组

  1. 过滤联合类型数组元素
    • 假设我们有一个包含字符串和数字的联合类型数组,我们想要过滤出所有的数字,并对它们进行求和。
let mixedArray: (string | number)[] = ['1', 2, '3', 4];
function sumNumbers(arr: (string | number)[]) {
    let numbers = arr.filter((value): value is number => typeof value === 'number');
    return numbers.reduce((acc, num) => acc + num, 0);
}
let result = sumNumbers(mixedArray);

在这个例子中,filter 方法的回调函数使用了自定义类型保护 typeof value === 'number',并返回 value is number,这使得 filter 方法返回的数组 numbers 被 TypeScript 推断为 number[] 类型,从而可以安全地调用 reduce 方法进行求和。

  1. 遍历联合类型数组并执行不同操作
    • 我们也可以遍历联合类型数组,并根据元素的类型执行不同的操作。
let mixedArray: (string | number)[] = ['1', 2, '3', 4];
function processArray(arr: (string | number)[]) {
    arr.forEach((value) => {
        if (typeof value ==='string') {
            console.log('String:', value.toUpperCase());
        } else {
            console.log('Number:', value.toFixed(2));
        }
    });
}
processArray(mixedArray);

这里,typeof 类型保护在 forEach 回调函数内帮助我们根据元素的实际类型执行不同的操作,而 TypeScript 会根据类型保护在不同的代码块内准确地推断 value 的类型。

处理嵌套对象和联合类型

  1. 访问嵌套对象的属性
    • 假设我们有一个嵌套对象,其中某个属性可能是不同类型的对象,我们需要访问该属性的特定方法。
interface Animal {
    name: string;
}
interface Dog extends Animal {
    bark(): void;
}
interface Cat extends Animal {
    meow(): void;
}
interface Zoo {
    animals: (Dog | Cat)[];
}
function visitZoo(zoo: Zoo) {
    zoo.animals.forEach((animal) => {
        if ('bark' in animal) {
            animal.bark();
        } else {
            animal.meow();
        }
    });
}

在这个例子中,in 类型保护在 forEach 回调函数内帮助我们根据 animal 对象实际包含的方法来调用相应的方法,TypeScript 会根据类型保护准确地推断 animal 在不同代码块内的类型。

  1. 处理多层嵌套联合类型
    • 考虑更复杂的多层嵌套联合类型场景。
interface A {
    type: 'a';
    value: string;
}
interface B {
    type: 'b';
    sub: {
        value: number;
    };
}
interface C {
    type: 'c';
    sub: {
        sub: {
            value: boolean;
        };
    };
}
type Union = A | B | C;
function processUnion(obj: Union) {
    if (obj.type === 'a') {
        console.log('A value:', obj.value);
    } else if (obj.type === 'b') {
        console.log('B value:', obj.sub.value);
    } else {
        console.log('C value:', obj.sub.sub.value);
    }
}

这里,通过对 obj.type 的类型保护,我们可以在不同的代码块内根据 obj 的实际类型访问其特定结构的属性,TypeScript 也会相应地进行准确的类型推断。

函数重载与类型保护和类型推断

  1. 函数重载结合类型保护
    • 函数重载允许我们为同一个函数定义多个不同的签名。结合类型保护,可以实现更灵活的功能。
function printValue(value: string): void;
function printValue(value: number): void;
function printValue(value: string | number) {
    if (typeof value ==='string') {
        console.log('String:', value.toUpperCase());
    } else {
        console.log('Number:', value.toFixed(2));
    }
}
printValue('hello');
printValue(10);

在这个例子中,函数重载定义了 printValue 函数接受 stringnumber 类型的参数。在函数实现中,通过 typeof 类型保护,我们可以根据参数的实际类型执行不同的操作,TypeScript 会根据调用时传入的参数类型选择合适的函数重载,并在函数内部根据类型保护进行准确的类型推断。

  1. 利用类型推断优化函数重载
    • 类型推断可以帮助我们在函数重载时更简洁地编写代码。
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any) {
    if (typeof a === 'number' && typeof b === 'number') {
        return a + b;
    } else if (typeof a ==='string' && typeof b ==='string') {
        return a + b;
    }
    return null;
}
let numResult = add(10, 20);
let strResult = add('hello', 'world');

这里,TypeScript 根据函数调用时传入的参数类型推断出应该使用哪个函数重载。在函数实现中,通过类型保护进一步细化类型,实现不同类型参数的正确处理,同时利用类型推断使得代码更加简洁。

类型保护与类型推断的最佳实践

保持类型保护的简洁性

  1. 避免复杂的类型保护逻辑
    • 类型保护应该尽量简单明了。复杂的类型保护逻辑可能会使代码难以理解和维护。例如,避免在类型保护函数中进行大量的计算或复杂的条件判断。
// 不好的示例,类型保护函数逻辑复杂
function isSpecialValue(value: any): value is { specialProp: string } {
    // 复杂的计算和判断逻辑
    if (typeof value === 'object' && 'prop1' in value && 'prop2' in value) {
        let result = value.prop1 + value.prop2;
        return result ==='specialResult';
    }
    return false;
}
// 好的示例,简洁的类型保护函数
function isStringValue(value: any): value is string {
    return typeof value ==='string';
}

在好的示例中,isStringValue 函数的逻辑非常简单,就是检查值是否为字符串类型,这样的类型保护函数易于理解和维护。

  1. 使用单一的类型保护原则
    • 尽量在一个类型保护中只检查一种类型特征。例如,不要在一个 typeof 类型保护中同时检查多种不同基本类型的条件。
// 不好的示例,混合多种类型检查
function handleValueBad(value: string | number | boolean) {
    if ((typeof value ==='string' || typeof value === 'number') && value) {
        // 复杂的条件,混合了字符串和数字的检查
    }
}
// 好的示例,单一类型检查
function handleValueGood(value: string | number | boolean) {
    if (typeof value ==='string') {
        // 处理字符串类型
    } else if (typeof value === 'number') {
        // 处理数字类型
    } else {
        // 处理布尔类型
    }
}

好的示例中,每个 if - else if 块只处理一种类型,使得代码逻辑清晰,易于维护。

充分利用类型推断

  1. 让 TypeScript 自动推断类型
    • 在大多数情况下,尽量让 TypeScript 自动推断变量和函数的类型,而不是过度显式地声明类型。这样可以减少代码冗余,同时让 TypeScript 的类型推断机制发挥作用。
// 不好的示例,过度显式声明类型
let num: number = 10;
function add(a: number, b: number): number {
    return a + b;
}
// 好的示例,利用类型推断
let num = 10;
function add(a, b) {
    return a + b;
}

在好的示例中,TypeScript 能够自动推断 num 的类型为 number,以及 add 函数的参数和返回值类型,代码更加简洁。

  1. 结合类型推断和类型保护
    • 在编写代码时,要充分利用类型推断和类型保护的协同作用。例如,在函数内部使用类型保护细化类型后,要利用 TypeScript 对细化类型的推断来编写更安全和简洁的代码。
function processValue(value: string | number) {
    if (typeof value ==='string') {
        let length = value.length;
        // 利用类型保护后的类型推断,这里 length 被推断为 number 类型
        console.log('Length of string:', length);
    } else {
        let fixedValue = value.toFixed(2);
        // 这里 fixedValue 被推断为 string 类型
        console.log('Fixed number:', fixedValue);
    }
}

在这个例子中,通过类型保护 typeof value ==='string',TypeScript 能够在不同的代码块内准确地推断出相关变量的类型,我们可以利用这些推断来编写更安全的代码。

测试类型保护和类型推断

  1. 单元测试类型保护函数
    • 对于自定义类型保护函数,应该编写单元测试来确保其正确性。例如,对于 isString 类型保护函数:
function isString(value: any): value is string {
    return typeof value ==='string';
}
// 单元测试示例(使用 Jest)
test('isString should correctly identify strings', () => {
    expect(isString('hello')).toBe(true);
    expect(isString(10)).toBe(false);
});

通过单元测试,可以保证类型保护函数在各种情况下都能正确工作,从而确保整个代码的类型安全性。

  1. 测试类型推断的正确性
    • 虽然类型推断是由 TypeScript 编译器自动完成的,但在复杂的代码结构中,也可以通过编写测试来验证类型推断是否符合预期。例如,在处理联合类型和类型保护的函数中:
function calculate(a: string | number, b: string | number) {
    if (typeof a === 'number' && typeof b === 'number') {
        return a + b;
    }
    return null;
}
// 单元测试示例(使用 Jest)
test('calculate should return number for number inputs', () => {
    let result = calculate(10, 20);
    expect(typeof result).toBe('number');
});
test('calculate should return null for non - number inputs', () => {
    let result = calculate('10', '20');
    expect(result).toBe(null);
});

这些测试可以帮助我们确保在使用类型保护和类型推断时,函数的行为符合预期,从而提高代码的可靠性。

总之,在 TypeScript 开发中,类型保护和类型推断是相辅相成的重要特性。通过遵循最佳实践,我们可以充分利用它们的优势,编写出更健壮、可读且易于维护的前端代码。无论是处理简单的变量类型,还是复杂的对象结构和函数重载,正确运用类型保护和类型推断都能提升我们的开发效率和代码质量。