TypeScript 类型守卫:类型谓词与类型缩小
TypeScript 的联合类型允许变量接受多种类型,但也带来了访问属性时的类型不确定性。类型守卫是一套运行时检查机制,用于在条件块中“缩小”类型的范围,确保代码安全。本文将介绍如何利用 typeof、in、instanceof 以及自定义的“类型谓词”来精准控制类型流。
1. 理解类型缩小机制
当 TypeScript 推断出一个变量可能是 string 或 number 时,直接调用 string 特有的方法(如 .toUpperCase())会报错。通过类型守卫,我们可以在特定代码块内告诉编译器:“在这个分支里,变量一定是 string”。
下面是类型守卫的工作原理流程:
2. 使用基础类型检查
TypeScript 原生支持一些检查表达式,能自动触发类型缩小。
2.1 使用 typeof 检查原始类型
处理 原始类型(如 string、number、boolean)的联合类型时,使用 typeof 运算符是最直接的方式。
编写 以下代码定义一个处理 string 或 number 的函数:
function printId(id: string | number) {
// 在这里,id 可能是 string 也可能是 number
if (typeof id === "string") {
// 此处 id 的类型被缩小为 string
console.log(id.toUpperCase());
} else {
// 此处 id 的类型被缩小为 number
console.log(id.toFixed(2));
}
}
注意 typeof 的返回值字符串必须与 TypeScript 类型一一对应。检查 "object" 时要小心,因为 null 和数组在 JavaScript 中也被视为 object。
2.2 使用 instanceof 检查类实例
处理 类的实例时,使用 instanceof 运算符。这会检查构造函数的 prototype 属性是否出现在对象的原型链中。
定义 两个类并编写处理函数:
class ErrorLog {
constructor(public message: string) {}
}
class WarnLog {
constructor(public code: number) {}
}
function handleLog(log: ErrorLog | WarnLog) {
if (log instanceof ErrorLog) {
// TypeScript 知道这里 log 是 ErrorLog
console.log(log.message);
} else {
// TypeScript 知道这里 log 是 WarnLog
console.log(log.code);
}
}
2.3 使用 in 操作符检查属性存在性
处理 没有共同父类的对象结构时,使用 in 操作符检查特定属性是否存在。
编写 逻辑来判断对象是“鸟”还是“鱼”:
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function moveInWater(animal: Bird | Fish) {
if ("swim" in animal) {
// 缩小为 Fish
animal.swim();
} else {
// 缩小为 Bird
animal.fly();
}
}
3. 掌握类型谓词
当逻辑复杂(例如需要检查多个字段或进行数值计算)无法通过简单的 typeof 或 in 表达时,我们需要自定义类型守卫函数。
3.1 定义类型谓词
类型谓词的语法格式为 parameterName is Type,它作为函数的返回值类型声明。
创建 一个判断变量是否为 Fish 的函数:
function isFish(pet: Bird | Fish): pet is Fish {
// 实现逻辑:检查是否有 swim 方法
return (pet as Fish).swim !== undefined;
}
这里 pet is Fish 就是类型谓词。它告诉 TypeScript:如果 isFish 返回 true,那么传入的 pet 参数就是 Fish 类型。
3.2 应用类型谓词
调用 自定义守卫函数来控制代码逻辑:
function getPetFood(pet: Bird | Fish): string {
if (isFish(pet)) {
// 此处 pet 被识别为 Fish
return "Fish Food";
} else {
// 此处 pet 被识别为 Bird
return "Bird Seed";
}
}
如果不用类型谓词,而直接写 (pet as Fish).swim,TypeScript 会认为这是类型断言,在 else 分支中依然无法确定 pet 是 Bird。使用 谓词能让编译器智能地在两个分支里正确切换类型。
4. 利用可辨识联合
这是一种结构性模式,要求联合类型中的每个对象都包含一个字面量类型的公共属性(通常是 kind 或 type),用于区分不同的成员。
定义 包含 kind 属性的接口:
interface Circle {
kind: "circle"; // 可辨识属性
radius: number;
}
interface Square {
kind: "square"; // 可辨识属性
sideLength: number;
}
type Shape = Circle | Square;
检查 kind 属性来缩小类型:
function getArea(shape: Shape): number {
if (shape.kind === "circle") {
// TypeScript 推断出 shape 是 Circle
return Math.PI * shape.radius * shape.radius;
} else {
// TypeScript 推断出 shape 是 Square
return shape.sideLength * shape.sideLength;
}
}
这种方式比 in 操作符更严格,因为它检查的是具体的字面量值,极大地减少了出错的可能性。
5. 类型守卫策略对比
下表总结了不同场景下应选择的守卫策略。
| 守卫类型 | 适用场景 | 语法示例 | 特点 |
|---|---|---|---|
typeof |
原始类型 (string, number) |
typeof x === "string" |
简单直接,仅限基本类型 |
instanceof |
类实例 (new Class()) |
x instanceof MyClass |
依赖原型链,不适用于接口 |
in |
对象结构 ({a: 1}, {b: 2}) |
"prop" in x |
检查属性名,通用性强 |
| 类型谓词 | 复杂逻辑判断 | x is Type |
可复用,逻辑封装在函数中 |
| 可辨识联合 | 带有标签的联合对象 | shape.kind === "circle" |
类型最安全,推荐模式 |
6. 断言函数
除了“检查”类型,我们有时需要“断言”类型,如果断言失败则抛出错误。这在处理从 API 获取的不确定数据时非常有用。
定义 一个断言函数,返回类型使用 asserts condition:
function assertIsString(val: unknown): asserts val is string {
if (typeof val !== "string") {
throw new Error("Not a string!");
}
}
调用 该函数处理输入:
function processInput(input: unknown) {
assertIsString(input);
// 执行过后,input 在此作用域内被视为 string
console.log(input.toUpperCase());
}
如果 input 不是字符串,程序会抛出异常停止执行;如果是字符串,代码继续运行且类型安全得到保证。

暂无评论,快来抢沙发吧!