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

Typescript中的类型保护

2023-10-165.7k 阅读

什么是类型保护

在 TypeScript 中,类型保护是一种机制,它允许我们在运行时检查变量的类型,并基于这个检查结果,TypeScript 编译器可以在该变量的特定作用域内,更准确地推断其类型。这使得我们能够编写更安全、更健壮的代码,尤其是在处理可能具有多种类型的变量时。

例如,假设我们有一个函数,它接收一个参数,这个参数可能是 string 类型,也可能是 number 类型。如果没有类型保护,在函数内部使用这个参数时,我们只能把它当作最宽泛的 string | number 类型来处理,无法充分利用每个具体类型的特性。而类型保护可以帮助我们在运行时确定参数的实际类型,从而让编译器知道在特定代码块内,这个参数的具体类型是什么。

类型保护的常见方式

typeof 类型保护

typeof 操作符在 JavaScript 中用于返回一个操作数的类型字符串。在 TypeScript 中,它也被用作一种类型保护机制。通过 typeof 进行类型保护,我们可以在运行时检查变量的类型,并根据检查结果在代码块内获得更精确的类型推断。

以下是一个简单的示例:

function printValue(value: string | number) {
    if (typeof value ==='string') {
        console.log(value.length); // 在这个代码块内,TypeScript 知道 value 是 string 类型,所以可以访问 length 属性
    } else {
        console.log(value.toFixed(2)); // 在这个代码块内,TypeScript 知道 value 是 number 类型,所以可以访问 toFixed 方法
    }
}

printValue('hello');
printValue(123);

在上述代码中,typeof value ==='string' 就是一个类型保护。当这个条件为真时,在相应的 if 代码块内,TypeScript 编译器会将 value 视为 string 类型,因此我们可以安全地访问 string 类型的属性和方法,比如 length。当条件为假时,在 else 代码块内,value 会被视为 number 类型,我们就可以访问 number 类型的方法,比如 toFixed

instanceof 类型保护

instanceof 操作符用于检查一个对象是否是某个构造函数的实例。在 TypeScript 中,它同样可以作为类型保护机制,帮助我们在运行时确定对象的具体类型。

假设有如下类继承结构:

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(); // 在这个代码块内,TypeScript 知道 animal 是 Dog 类型,所以可以调用 bark 方法
    } else if (animal instanceof Cat) {
        animal.meow(); // 在这个代码块内,TypeScript 知道 animal 是 Cat 类型,所以可以调用 meow 方法
    }
}

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

handleAnimal(myDog);
handleAnimal(myCat);

在上述代码中,animal instanceof Doganimal instanceof Cat 都是类型保护。当 animal instanceof Dog 为真时,在相应的 if 代码块内,animal 会被视为 Dog 类型,我们就可以调用 Dog 类特有的 bark 方法。同理,当 animal instanceof Cat 为真时,animal 会被视为 Cat 类型,我们可以调用 meow 方法。

in 类型保护

in 操作符用于检查对象是否包含某个属性。在 TypeScript 中,我们可以利用 in 操作符来实现类型保护,通过检查对象是否存在特定属性来推断对象的类型。

例如:

interface WithLength {
    length: number;
}

interface StringLike extends WithLength {
    toUpperCase(): string;
}

interface NumberArray extends WithLength {
    [index: number]: number;
}

function printLength(value: StringLike | NumberArray) {
    if ('toUpperCase' in value) {
        console.log((value as StringLike).toUpperCase()); // 在这个代码块内,TypeScript 知道 value 是 StringLike 类型,所以可以调用 toUpperCase 方法
    } else {
        console.log(value.length); // 在这个代码块内,TypeScript 知道 value 是 NumberArray 类型,所以可以访问 length 属性
    }
}

const myString: StringLike = 'hello';
const myArray: NumberArray = [1, 2, 3];

printLength(myString);
printLength(myArray);

在上述代码中,'toUpperCase' in value 是一个类型保护。当这个条件为真时,在相应的 if 代码块内,TypeScript 编译器会将 value 视为 StringLike 类型,因为只有 StringLike 类型的对象才具有 toUpperCase 方法。当条件为假时,value 会被视为 NumberArray 类型。

用户自定义类型保护函数

除了使用 typeofinstanceofin 这些内置的类型保护方式外,TypeScript 还允许我们定义自己的类型保护函数。用户自定义类型保护函数必须返回一个类型谓词,即一个形如 parameterName is Type 的表达式,其中 parameterName 是函数参数的名称,Type 是要判断的类型。

以下是一个示例:

interface Bird {
    fly(): void;
    layEggs(): void;
}

interface Fish {
    swim(): void;
    layEggs(): void;
}

function isBird(animal: Bird | Fish): animal is Bird {
    return (animal as Bird).fly!== undefined;
}

function handleAnimal(animal: Bird | Fish) {
    if (isBird(animal)) {
        animal.fly(); // 在这个代码块内,TypeScript 知道 animal 是 Bird 类型,所以可以调用 fly 方法
        animal.layEggs();
    } else {
        animal.swim(); // 在这个代码块内,TypeScript 知道 animal 是 Fish 类型,所以可以调用 swim 方法
        animal.layEggs();
    }
}

const myBird: Bird = {
    fly() {
        console.log('Flying...');
    },
    layEggs() {
        console.log('Laying eggs...');
    }
};

const myFish: Fish = {
    swim() {
        console.log('Swimming...');
    },
    layEggs() {
        console.log('Laying eggs...');
    }
};

handleAnimal(myBird);
handleAnimal(myFish);

在上述代码中,isBird 函数就是一个用户自定义类型保护函数。它接收一个 Bird | Fish 类型的参数 animal,并通过检查 animal 是否具有 fly 方法来判断它是否是 Bird 类型。如果 isBird 函数返回 true,在相应的 if 代码块内,animal 会被视为 Bird 类型,我们就可以调用 Bird 类型的方法。如果返回 falseanimal 会被视为 Fish 类型。

类型保护与类型断言的区别

类型断言是一种手动告诉编译器某个变量的类型的方式,它不进行运行时检查。例如:

let value: any = 'hello';
let length: number = (value as string).length;

在这个例子中,我们使用 as 关键字将 value 断言为 string 类型,编译器会相信我们的断言,而不会在运行时检查 value 是否真的是 string 类型。如果 value 实际上不是 string 类型,运行时可能会出错。

而类型保护是在运行时进行类型检查,根据检查结果让编译器在特定代码块内更准确地推断类型。例如:

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

这里通过 typeof 进行类型保护,在运行时检查 value 的类型,然后编译器根据检查结果在不同的代码块内有不同的类型推断,从而使代码更加安全。

类型保护在联合类型和交叉类型中的应用

联合类型中的类型保护

联合类型表示一个变量可以是多种类型中的一种。类型保护在联合类型中非常有用,它可以帮助我们在使用联合类型的变量时,确定其具体类型。

例如,我们有一个联合类型 string | number

function add(a: string | number, b: string | number) {
    if (typeof a === 'number' && typeof b === 'number') {
        return a + b;
    } else if (typeof a ==='string' && typeof b ==='string') {
        return a + b;
    } else {
        throw new Error('Both arguments must be of the same type');
    }
}

console.log(add(1, 2));
console.log(add('hello', 'world'));
// console.log(add(1, 'world')); // 这行代码会抛出错误

在上述 add 函数中,我们通过 typeof 类型保护来检查 ab 的类型。如果它们都是 number 类型,就执行数字相加操作;如果都是 string 类型,就执行字符串拼接操作。如果类型不匹配,就抛出错误。这样可以确保在处理联合类型时,代码的行为是可预测和安全的。

交叉类型中的类型保护

交叉类型表示一个变量同时具有多种类型的特性。虽然交叉类型相对联合类型使用频率较低,但类型保护同样可以在交叉类型中发挥作用。

例如:

interface A {
    a: string;
}

interface B {
    b: number;
}

function handleAB(value: A & B) {
    console.log(value.a);
    console.log(value.b);
}

const myAB: A & B = {
    a: 'hello',
    b: 123
};

handleAB(myAB);

在上述代码中,valueA & B 交叉类型。由于 A & B 类型的对象必然同时具有 AB 类型的属性,所以这里不需要像联合类型那样进行复杂的类型保护。但在实际应用中,如果交叉类型的来源较为复杂,可能也需要使用类型保护来确保对象确实具有所需的属性和方法。

类型保护与函数重载

函数重载允许我们为同一个函数定义多个不同的函数签名,根据传入参数的不同类型和数量,编译器会选择合适的函数实现。类型保护在函数重载中也有重要的应用。

例如:

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

printValue('hello');
printValue(123);

在上述代码中,我们定义了两个函数重载签名,一个接收 string 类型参数,另一个接收 number 类型参数。而实际的函数实现中,通过 typeof 类型保护来根据参数的实际类型进行不同的操作。这样,编译器可以根据调用 printValue 函数时传入的参数类型,准确地选择合适的函数重载版本,并在函数内部利用类型保护进行安全的操作。

类型保护在泛型中的应用

泛型允许我们在定义函数、类或接口时,使用类型参数来表示一种通用的类型,而不是具体的类型。类型保护在泛型中同样有着重要的作用,可以帮助我们在泛型代码中更准确地处理不同类型的值。

例如,我们定义一个泛型函数,它接收一个数组,并返回数组中的第一个元素:

function getFirst<T>(array: T[]): T | undefined {
    return array.length > 0? array[0] : undefined;
}

const numbers = [1, 2, 3];
const firstNumber = getFirst(numbers);
if (typeof firstNumber === 'number') {
    console.log(firstNumber.toFixed(2));
}

const strings = ['a', 'b', 'c'];
const firstString = getFirst(strings);
if (typeof firstString ==='string') {
    console.log(firstString.length);
}

在上述代码中,getFirst 函数是一个泛型函数,它返回的类型是 T | undefined。在调用 getFirst 函数后,我们使用 typeof 类型保护来检查返回值的类型,以便在后续代码中可以安全地访问相应类型的属性和方法。这样,通过类型保护,我们可以在泛型代码中针对不同的实际类型进行特定的操作,增强了代码的通用性和安全性。

类型保护的注意事项

类型保护的作用域

类型保护的作用域仅限于其所在的代码块。例如:

function printValue(value: string | number) {
    let localVar: string | number;
    if (typeof value ==='string') {
        localVar = value;
        console.log(localVar.length); // 这里 localVar 被推断为 string 类型
    }
    // 在这里,localVar 的类型又变回 string | number,因为类型保护的作用域仅限于 if 代码块内
    // console.log(localVar.length); // 这行代码会报错,因为无法确定 localVar 的类型
}

if 代码块内,localVar 会根据类型保护被推断为 string 类型,但在 if 代码块外部,localVar 的类型又变回了 string | number,所以不能直接访问 length 属性。

复杂类型的类型保护

对于复杂类型,如对象的嵌套结构或具有多种可能类型的复杂联合类型,类型保护可能会变得更加复杂。我们需要仔细考虑如何通过属性检查、方法检查等方式来准确地确定类型。

例如:

interface Shape {
    kind: string;
}

interface Circle extends Shape {
    kind: 'circle';
    radius: number;
}

interface Square extends Shape {
    kind:'square';
    sideLength: number;
}

function draw(shape: Circle | Square) {
    if (shape.kind === 'circle') {
        console.log(`Drawing a circle with radius ${shape.radius}`);
    } else {
        console.log(`Drawing a square with side length ${shape.sideLength}`);
    }
}

const myCircle: Circle = {
    kind: 'circle',
    radius: 5
};

const mySquare: Square = {
    kind:'square',
    sideLength: 4
};

draw(myCircle);
draw(mySquare);

在上述代码中,我们通过检查 kind 属性来区分 CircleSquare 类型。对于更复杂的情况,可能需要检查多个属性或结合其他类型保护方式来准确确定类型。

与类型兼容性的关系

类型保护与类型兼容性密切相关。类型保护可以帮助我们在处理可能具有多种类型的变量时,确保代码在运行时的行为符合预期,同时也与 TypeScript 编译器对类型兼容性的判断相互影响。例如,在函数参数传递时,如果通过类型保护确定了参数的具体类型,那么在函数内部对该参数的使用就需要符合该具体类型的兼容性规则。

类型保护在实际项目中的应用场景

数据获取与处理

在实际项目中,我们经常从 API 接口获取数据,这些数据的类型可能并不总是确定的。例如,一个 API 可能返回一个对象,这个对象可能是用户信息对象,也可能是错误信息对象。通过类型保护,我们可以在接收到数据后,准确地判断数据的类型,并进行相应的处理。

interface User {
    id: number;
    name: string;
}

interface ErrorResponse {
    errorCode: number;
    errorMessage: string;
}

function handleResponse(response: User | ErrorResponse) {
    if ('id' in response) {
        console.log(`Welcome, ${response.name}!`);
    } else {
        console.log(`Error: ${response.errorMessage}`);
    }
}

const userResponse: User = {
    id: 1,
    name: 'John'
};

const errorResponse: ErrorResponse = {
    errorCode: 404,
    errorMessage: 'Not Found'
};

handleResponse(userResponse);
handleResponse(errorResponse);

在上述代码中,我们通过 'id' in response 类型保护来判断接收到的响应是用户信息还是错误信息,并进行相应的处理。

组件库开发

在组件库开发中,组件可能需要接收不同类型的属性。例如,一个按钮组件可能接收字符串类型的文本,也可能接收一个 ReactNode 类型的子元素。通过类型保护,我们可以在组件内部根据属性的实际类型进行不同的渲染逻辑。

import React from'react';

interface ButtonProps {
    text?: string;
    children?: React.ReactNode;
}

const Button: React.FC<ButtonProps> = ({ text, children }) => {
    if (text) {
        return <button>{text}</button>;
    } else if (children) {
        return <button>{children}</button>;
    }
    return null;
};

export default Button;

在上述 React 组件代码中,我们通过检查 textchildren 属性是否存在,来决定按钮的渲染内容,这就是类型保护在组件库开发中的应用。

表单验证

在处理表单数据时,我们需要对用户输入的数据进行验证。例如,一个表单可能包含年龄字段,用户输入的可能是数字,也可能是无效的字符串。通过类型保护,我们可以在验证函数中判断输入的类型,并进行相应的验证逻辑。

function validateAge(age: string | number) {
    if (typeof age === 'number') {
        return age >= 0 && age <= 120;
    } else {
        const num = parseInt(age, 10);
        return!isNaN(num) && num >= 0 && num <= 120;
    }
}

console.log(validateAge(25));
console.log(validateAge('25'));
console.log(validateAge('twenty five'));

在上述代码中,我们通过 typeof 类型保护来判断 age 的类型,并针对不同类型进行相应的年龄验证逻辑。

通过以上详细的介绍,我们对 TypeScript 中的类型保护有了全面深入的了解,包括其概念、常见方式、与其他类型相关特性的关系以及在实际项目中的应用场景等。在编写 TypeScript 代码时,合理运用类型保护可以大大提高代码的安全性和健壮性。