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

TypeScript自定义类型保护函数设计模式

2022-06-237.8k 阅读

什么是类型保护函数

在TypeScript中,类型保护函数是一种特殊的函数,它允许我们在运行时检查某个值的类型,并基于这个检查结果在后续代码中缩小该值的类型范围。这对于处理联合类型(union types)时非常有用。例如,我们有一个联合类型string | number,如果没有类型保护,很难在代码中安全地对这个联合类型的值执行特定类型的操作。类型保护函数就是用来解决这个问题的。

类型保护函数的基本语法

类型保护函数的返回值必须是一个类型谓词。类型谓词的语法是parameterName is Type,其中parameterName是函数参数的名称,Type是要检查的类型。下面是一个简单的示例:

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

let myValue: string | number = 'hello';
if (isString(myValue)) {
    console.log(myValue.length); // 这里myValue的类型被缩小为string,可以安全访问length属性
}

在上述代码中,isString函数接受一个string | number类型的参数value,并返回一个类型谓词value is string。当isString函数返回true时,TypeScript编译器就知道在if块内,myValue的类型是string,从而允许我们安全地访问string类型的属性和方法,如length

设计类型保护函数的模式

基于类型检查的模式

这是最常见的模式,通过typeofinstanceof等操作符来检查值的类型。

  1. 使用typeof进行类型检查
    • 除了前面的isString示例,我们还可以创建检查number类型的函数:
function isNumber(value: string | number): value is number {
    return typeof value === 'number';
}

let anotherValue: string | number = 42;
if (isNumber(anotherValue)) {
    console.log(anotherValue.toFixed(2)); // 在if块内,anotherValue的类型被缩小为number
}
  • 对于更复杂的联合类型,比如string | number | boolean,我们可以分别创建类型保护函数:
function isBoolean(value: string | number | boolean): value is boolean {
    return typeof value === 'boolean';
}

let complexValue: string | number | boolean = true;
if (isBoolean(complexValue)) {
    console.log(complexValue? 'Yes' : 'No');
}
  1. 使用instanceof进行类型检查
    • 当处理类的实例时,instanceof操作符非常有用。假设我们有两个类DogCat,并且有一个联合类型Dog | Cat
class Dog {
    bark() {
        console.log('Woof!');
    }
}

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

function isDog(pet: Dog | Cat): pet is Dog {
    return pet instanceof Dog;
}

let myPet: Dog | Cat = new Dog();
if (isDog(myPet)) {
    myPet.bark(); // 在if块内,myPet的类型被缩小为Dog
}

基于属性检查的模式

有时候,对象可能具有特定的属性,我们可以通过检查这些属性来确定对象的类型。

  1. 简单属性检查
    • 假设我们有两种类型的对象,一种是具有name属性的Person类型,另一种是具有model属性的Car类型,它们组成联合类型Person | Car
interface Person {
    name: string;
    age: number;
}

interface Car {
    model: string;
    year: number;
}

function isPerson(obj: Person | Car): obj is Person {
    return 'name' in obj;
}

let myObj: Person | Car = { name: 'John', age: 30 };
if (isPerson(myObj)) {
    console.log(`Hello, ${myObj.name}`);
}
  • 在上述代码中,isPerson函数通过检查obj对象是否具有name属性来确定它是否是Person类型。
  1. 复杂属性检查
    • 对于更复杂的情况,我们可能需要检查多个属性或属性的类型。比如,假设我们有RectangleCircle两种形状,它们组成联合类型Rectangle | Circle
interface Rectangle {
    type: 'rectangle';
    width: number;
    height: number;
}

interface Circle {
    type: 'circle';
    radius: number;
}

function isRectangle(shape: Rectangle | Circle): shape is Rectangle {
    return shape.type ==='rectangle' && typeof shape.width === 'number' && typeof shape.height === 'number';
}

let myShape: Rectangle | Circle = { type:'rectangle', width: 10, height: 20 };
if (isRectangle(myShape)) {
    console.log(`Rectangle area: ${myShape.width * myShape.height}`);
}

基于函数调用的模式

有时候,我们可以通过调用一个函数来确定值的类型。这种模式在处理异步操作或需要额外逻辑判断时很有用。

  1. 异步函数调用
    • 假设我们有一个异步函数fetchData,它可能返回User类型的数据或null。我们可以创建一个类型保护函数来确保返回的数据是User类型:
interface User {
    id: number;
    name: string;
}

async function fetchData(): Promise<User | null> {
    // 模拟异步操作,这里返回一个模拟的User对象
    return { id: 1, name: 'Alice' };
}

function isUser(data: User | null): data is User {
    return data!== null;
}

fetchData().then(data => {
    if (isUser(data)) {
        console.log(`User name: ${data.name}`);
    }
});
  • 在上述代码中,isUser函数通过检查data是否为null来确定它是否是User类型。
  1. 基于复杂逻辑的函数调用
    • 假设我们有一个函数parseValue,它可能返回numberstring,并且根据值的内容有不同的解析逻辑。我们可以创建一个类型保护函数:
function parseValue(input: string): number | string {
    if (/^\d+$/.test(input)) {
        return parseInt(input);
    }
    return input;
}

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

let parsed = parseValue('42');
if (isParsedNumber(parsed)) {
    console.log(`Parsed number: ${parsed * 2}`);
}

类型保护函数与泛型

类型保护函数与泛型结合可以实现更通用的类型检查。

  1. 简单泛型类型保护
    • 假设我们有一个函数identity,它返回传入的值。我们可以创建一个类型保护函数来检查返回值是否是特定类型:
function identity<T>(arg: T): T {
    return arg;
}

function isStringValue<T>(value: T): value is string & T {
    return typeof value ==='string';
}

let result = identity<string | number>('test');
if (isStringValue(result)) {
    console.log(result.length);
}
  • 在上述代码中,isStringValue函数使用泛型T,并且通过类型谓词value is string & T来检查value是否是string类型。
  1. 泛型类与类型保护
    • 考虑一个简单的泛型类Box,它可以存储任意类型的值。我们可以创建类型保护函数来检查Box中存储的值的类型:
class Box<T> {
    constructor(private value: T) {}
    getValue() {
        return this.value;
    }
}

function isNumberBox(box: Box<number | string>): box is Box<number> {
    return typeof box.getValue() === 'number';
}

let numberBox = new Box<number | string>(42);
if (isNumberBox(numberBox)) {
    console.log(`Box contains number: ${numberBox.getValue()}`);
}

类型保护函数在数组中的应用

当处理数组中的联合类型时,类型保护函数同样非常有用。

  1. 过滤数组元素
    • 假设我们有一个数组,其中元素是string | number类型,我们想过滤出string类型的元素:
function isString(value: string | number): value is string {
    return typeof value ==='string';
}

let mixedArray: (string | number)[] = ['hello', 42, 'world'];
let stringArray = mixedArray.filter(isString);
console.log(stringArray.map(str => str.length));
  • 在上述代码中,filter方法使用isString类型保护函数来过滤出string类型的元素,从而得到一个纯string类型的数组。
  1. 遍历数组并执行特定类型操作
    • 假设我们有一个数组,元素是Dog | Cat类型,我们想遍历数组并对Dog类型的元素调用bark方法,对Cat类型的元素调用meow方法:
class Dog {
    bark() {
        console.log('Woof!');
    }
}

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

function isDog(pet: Dog | Cat): pet is Dog {
    return pet instanceof Dog;
}

let pets: (Dog | Cat)[] = [new Dog(), new Cat()];
pets.forEach(pet => {
    if (isDog(pet)) {
        pet.bark();
    } else {
        pet.meow();
    }
});

类型保护函数与类型别名和接口

类型保护函数与类型别名和接口结合使用,可以更清晰地定义和使用类型。

  1. 与类型别名结合
    • 假设我们有一个类型别名MaybeNumber,它表示numbernull。我们可以创建一个类型保护函数来检查值是否是number
type MaybeNumber = number | null;

function isNumberValue(value: MaybeNumber): value is number {
    return value!== null;
}

let numOrNull: MaybeNumber = 42;
if (isNumberValue(numOrNull)) {
    console.log(numOrNull.toFixed(2));
}
  1. 与接口结合
    • 假设我们有两个接口SquareTriangle,它们组成联合类型Shape。我们可以创建类型保护函数来区分它们:
interface Square {
    type:'square';
    sideLength: number;
}

interface Triangle {
    type: 'triangle';
    base: number;
    height: number;
}

type Shape = Square | Triangle;

function isSquare(shape: Shape): shape is Square {
    return shape.type ==='square';
}

let myShape: Shape = { type:'square', sideLength: 10 };
if (isSquare(myShape)) {
    console.log(`Square area: ${myShape.sideLength * myShape.sideLength}`);
}

类型保护函数的局限性

虽然类型保护函数非常强大,但也有一些局限性。

  1. 运行时检查
    • 类型保护函数依赖于运行时检查。这意味着如果在编译时就能确定类型,类型保护函数就没有作用。例如:
let myNumber: number = 42;
function isNumber(value: string | number): value is number {
    return typeof value === 'number';
}
// 这里myNumber在编译时就已知是number类型,isNumber函数不会起到作用
  1. 复杂类型的局限性
    • 对于非常复杂的类型,特别是涉及到递归类型或深度嵌套类型,编写有效的类型保护函数可能会很困难。例如,假设我们有一个递归类型表示树结构:
interface TreeNode {
    value: number;
    children?: TreeNode[];
}

// 要编写一个有效的类型保护函数来检查特定类型的TreeNode子结构会比较复杂
  1. 与其他类型系统特性的交互
    • 类型保护函数需要与其他TypeScript类型系统特性(如类型推断、泛型等)正确交互。有时候,不正确的使用可能会导致类型错误或难以理解的代码行为。例如,在复杂的泛型场景下,类型保护函数可能无法按预期缩小类型范围。

最佳实践

  1. 保持函数简洁
    • 类型保护函数应该尽可能简洁,只专注于检查类型。复杂的逻辑应该放在其他函数中。例如:
function isPositiveNumber(value: number | string): value is number {
    if (typeof value === 'number') {
        return value > 0;
    }
    return false;
}
// 更好的方式是分离逻辑
function isNumberValue(value: number | string): value is number {
    return typeof value === 'number';
}

function isPositive(num: number): boolean {
    return num > 0;
}

let value: number | string = 42;
if (isNumberValue(value) && isPositive(value)) {
    console.log('Positive number');
}
  1. 命名规范

    • 类型保护函数的命名应该清晰地表明它检查的类型。例如,isStringisNumberisUser等命名能够让代码阅读者快速理解函数的用途。
  2. 测试类型保护函数

    • 由于类型保护函数在运行时对类型检查起关键作用,对它们进行单元测试是很重要的。可以使用测试框架(如Jest)来测试类型保护函数的各种输入情况,确保它们按预期工作。例如:
import { isString } from './typeGuards';

test('isString should return true for string values', () => {
    expect(isString('test')).toBe(true);
});

test('isString should return false for non - string values', () => {
    expect(isString(42)).toBe(false);
});
  1. 文档化
    • 对于复杂的类型保护函数,特别是那些依赖于特定业务逻辑或复杂类型检查的函数,应该进行文档化。可以使用JSDoc等工具来添加注释,解释函数的作用、输入和输出。例如:
/**
 * Checks if the given value is a valid email address string.
 * @param value The value to check.
 * @returns True if the value is a valid email address string, false otherwise.
 */
function isEmail(value: string | number): value is string {
    if (typeof value ==='string' && /^[\w -]+(\.[\w -]+)*@([\w -]+\.)+[a-zA - Z]{2,7}$/.test(value)) {
        return true;
    }
    return false;
}

通过合理设计和使用TypeScript自定义类型保护函数,我们能够在处理联合类型和复杂类型时,编写出更健壮、类型安全的代码。同时,了解其局限性并遵循最佳实践,可以让我们更好地利用这一强大的功能。