一、啥是类型谓词和自定义类型守卫

在 TypeScript 里,类型谓词和自定义类型守卫可是很重要的东西。简单来说,类型谓词就是一种特殊的函数返回值类型,它能帮助 TypeScript 编译器更精准地判断变量的类型。而自定义类型守卫呢,就是咱们自己写的函数,用类型谓词来告诉编译器某个变量是不是符合特定类型。

举个例子,假如我们有两种类型:动物和人类。我们可以写个自定义类型守卫来判断一个变量是动物还是人类。

// 技术栈:TypeScript
// 定义动物类型
type Animal = {
    name: string;
    species: string;
};

// 定义人类类型
type Human = {
    name: string;
    age: number;
};

// 自定义类型守卫函数
function isAnimal(value: Animal | Human): value is Animal {
    return (value as Animal).species!== undefined;
}

// 使用类型守卫
const myAnimal: Animal = { name: 'Dog', species: 'Canine' };
const myHuman: Human = { name: 'John', age: 30 };

if (isAnimal(myAnimal)) {
    console.log(`${myAnimal.name} is an animal of species ${myAnimal.species}`);
}

if (isAnimal(myHuman)) {
    console.log(`${myHuman.name} is an animal`);
} else {
    console.log(`${myHuman.name} is a human`);
}

在这个例子里,isAnimal 就是自定义类型守卫函数,value is Animal 就是类型谓词。它会检查传入的值是否有 species 属性,如果有就认为是动物类型。

二、类型谓词的工作原理

类型谓词的核心就是让编译器在特定条件下,把变量的类型缩小到某个具体类型。当我们调用一个带有类型谓词的函数时,编译器会根据函数的返回值来更新变量的类型。

还是上面的例子,当 isAnimal 函数返回 true 时,编译器就知道传入的变量是 Animal 类型;返回 false 时,就知道是 Human 类型。

// 技术栈:TypeScript
function isString(value: any): value is string {
    return typeof value === 'string';
}

let myValue: any = 'Hello';
if (isString(myValue)) {
    // 这里 myValue 被编译器认为是 string 类型
    console.log(myValue.toUpperCase()); 
}

在这个例子中,isString 函数使用类型谓词 value is string。当函数返回 true 时,编译器就把 myValue 的类型从 any 缩小到 string,这样我们就可以安全地调用 toUpperCase 方法了。

三、自定义类型守卫的应用场景

1. 处理联合类型

联合类型可以让一个变量拥有多种可能的类型,但在使用时我们常常需要判断具体是哪种类型。自定义类型守卫就能很好地解决这个问题。

// 技术栈:TypeScript
type Circle = {
    kind: 'circle';
    radius: number;
};

type Square = {
    kind: 'square';
    sideLength: number;
};

type Shape = Circle | Square;

function isCircle(shape: Shape): shape is Circle {
    return shape.kind === 'circle';
}

function calculateArea(shape: Shape) {
    if (isCircle(shape)) {
        return Math.PI * shape.radius * shape.radius;
    } else {
        return shape.sideLength * shape.sideLength;
    }
}

const myCircle: Circle = { kind: 'circle', radius: 5 };
const area = calculateArea(myCircle);
console.log(`The area of the circle is ${area}`);

在这个例子中,isCircle 函数作为自定义类型守卫,帮助我们判断 shapeCircle 还是 Square,从而正确计算面积。

2. 验证 API 响应

当我们从 API 获取数据时,数据的类型可能不确定。自定义类型守卫可以帮助我们验证数据是否符合预期。

// 技术栈:TypeScript
interface User {
    id: number;
    name: string;
    email: string;
}

function isUser(data: any): data is User {
    return (
        typeof data === 'object' &&
        data!== null &&
        typeof data.id === 'number' &&
        typeof data.name === 'string' &&
        typeof data.email === 'string'
    );
}

// 模拟 API 响应
const apiResponse = { id: 1, name: 'Alice', email: 'alice@example.com' };

if (isUser(apiResponse)) {
    console.log(`User ${apiResponse.name} with ID ${apiResponse.id} is valid`);
} else {
    console.log('Invalid user data');
}

这里的 isUser 函数验证了 API 响应的数据是否符合 User 接口的定义。

四、技术优缺点分析

优点

  • 类型安全:自定义类型守卫能让 TypeScript 编译器在编译阶段发现类型错误,避免运行时错误。就像前面的例子,通过类型守卫,我们可以确保在使用变量时其类型是正确的。
  • 代码可读性:使用类型守卫可以让代码更清晰,明确表达变量的类型。比如在处理联合类型时,类型守卫让我们很容易看出不同类型的处理逻辑。

缺点

  • 增加代码复杂度:编写自定义类型守卫需要额外的代码,尤其是复杂的类型判断逻辑,会让代码变得更复杂。
  • 维护成本:如果类型定义发生变化,类型守卫函数也需要相应修改,增加了维护成本。

五、注意事项

1. 类型谓词的准确性

类型谓词的判断逻辑要准确,否则会导致类型判断错误。比如在 isAnimal 函数中,如果判断条件写错,就可能把人类误判为动物。

2. 性能问题

复杂的类型守卫函数可能会影响性能,尤其是在大量数据处理时。所以要尽量优化类型守卫的逻辑。

3. 兼容性

类型守卫要考虑不同类型的兼容性。比如在处理联合类型时,要确保类型守卫能正确区分不同类型。

六、文章总结

通过自定义类型守卫和类型谓词,我们可以让 TypeScript 编译器更智能地处理类型。类型谓词能帮助编译器缩小变量的类型范围,自定义类型守卫则是我们实现类型判断的工具。在实际开发中,类型守卫在处理联合类型、验证 API 响应等场景中非常有用。不过,我们也要注意类型谓词的准确性、性能问题和兼容性。掌握好类型谓词和自定义类型守卫,能让我们的 TypeScript 代码更安全、更易读。