在 TypeScript 中,联合类型使用起来是非常灵活的,它允许一个变量具备多种可能的类型。但是,这种灵活性也带来了一个非常致命的痛点:当你访问联合类型上的属性时,TypeScript 只允许你访问所有类型中 “共有” 的属性。
想象一下,假如你定义了一个变量,它的类型是 “string | number”。如果直接调用它的 “.length” 属性,TypeScript 会直接报错拦截,这是因为 number 身上压根就没有 length属性。
为了在复杂的业务分支中安全地剥离出具体的类型,TypeScript 提供了一套十分优雅的机制:类型保护(Type Guards)。
TypeScript 基础类型保护:typeof 与 instanceof
在 JavaScript 原生语法中,我们就经常使用 typeof 和 instanceof 来判断变量的类型。TypeScript 同样继承了这两个运行时操作符,并将它们无缝融合到了自己的静态类型推断系统中。
1. typeof 用于处理基础类型
当我们使用 typeof 在 if 语句中进行判断时,TypeScript 会自动在当前的代码块中,将联合类型 “收窄” 为具体的类型。
示例 1:使用 typeof 收窄类型
function printInfo(info: string | number) {
if (typeof info === "string") {
console.log("这是一个字符串,长度为:", info.length);
} else {
console.log("这是一个数字,保留两位小数:", info.toFixed(2));
}
}
printInfo("绿叶网");
printInfo(2026);运行结果如下。
这是一个字符串,长度为:3
这是一个数字,保留两位小数:2026.00分析:
对于这个例子来说,下面写法是有问题的:
function printInfo(info: string | number) {
// 报错:类型 “string | number” 上不存在属性 “length”
console.log(info.length);
}2. instanceof 用于处理对象类型
如果你的联合类型是由具体的 “类(Class)” 构成的,此时使用 typeof 就行不通了,因为它只会返回 "object"。对于实例化对象,我们需要使用 instanceof 操作符才行。
示例 2:使用 instanceof 收窄类
class Dog {
public bark() {
console.log("汪汪汪!");
}
}
class Cat {
public meow() {
console.log("喵喵喵!");
}
}
function handlePet(pet: Dog | Cat) {
if (pet instanceof Dog) {
// TypeScript 知道 pet 是 Dog 的实例
pet.bark();
} else {
// TypeScript 推断出 pet 是 Cat
pet.meow();
}
}
handlePet(new Dog());运行结果如下。
汪汪汪!分析:
在这个例子中,因为 Dog 和 Cat 是通过 class 实例化出来的真实对象,它们在运行时的 JavaScript 原型链上是确确实实存在的。所以我们可以安全地使用 instanceof 操作符来进行判断。
当 pet instanceof Dog 判断为 true 时,TypeScript 会极其聪明地将 pet 的类型从宽泛的联合类型收窄为精确的 Dog 类型。此时,在 if 的代码块内部,TypeScript 就会允许你安全地调用 Dog 类独有的 bark() 方法,而不会报错了。
TypeScript 进阶类型保护:in
在现代前端开发(Vue 或 React)中,我们很少会使用 class 来定义数据结构,绝大多数情况下都是使用 interface(接口)或 type(类型别名)来描述普通的 JSON 对象。
这就引出了一个巨大的难题:接口在 TypeScript 编译后是会被完全抹除的,它根本不存在于运行时的 JavaScript 中!也就是说,我们绝对不能对一个接口对象使用 instanceof。那此时应该怎么办呢?
在 TypeScript 中,我们可以使用 in 关键字来检查对象身上是否存在某个 “独有” 的属性。
示例 3:使用 in 收窄接口类型
interface User {
name: string;
email: string;
}
interface Admin {
name: string;
role: string; // Admin 独有的属性
}
function login(person: User | Admin) {
if ("role" in person) {
// TypeScript 通过 "role" 属性,推断出这是 Admin 类型
console.log("管理员登录,拥有权限:", person.role);
} else {
// 自动推断为 User
console.log("普通用户登录,邮箱:", person.email);
}
}
login({ name: "Jack", role: "SuperAdmin" });运行结果如下。
管理员登录,拥有权限:SuperAdmin分析:
in 关键字是处理接口联合类型时使用频率非常高。通过寻找某个类型 “独有” 的特征属性,TypeScript 就能在底层帮你完成类型的收窄与过滤。
TypeScript 相等性收窄 (Equality Narrowing)
除了使用内置操作符(typeof、instanceof、in)之外,在 TypeScript 开发中,我们最常用的另一种类型保护方式,就是直接通过判断两个值是否 “全等(===)” 来进行类型收窄,这种方式也叫做 “相等性收窄 (Equality Narrowing)”。
示例 4:使用 “===” 进行相等性收窄
// 定义一个网络请求状态的联合类型
type RequestStatus = "idle" | "loading" | "success" | "error";
function handleResponse(status: RequestStatus) {
if (status === "success") {
// 当判断 status 绝对等于 "success" 时
// TypeScript 会自动将 status 收窄为字面量类型 "success"
console.log("请求成功,正在渲染数据...");
} else if (status === "error") {
// TypeScript 自动推断这里是 "error"
console.log("请求失败,请稍后重试!");
} else {
// TypeScript 会推断出这里只剩下 "idle" | "loading"
console.log("其他状态:", status);
}
}
handleResponse("success");运行结果如下。
请求成功,正在渲染数据...分析:
在这个例子中,status 原本是一个包含 4 种状态的联合类型。当我们使用 if (status === "success") 时,TypeScript 编译器非常聪明,它知道在这个 if 代码块里面,status 的值必定是 "success"。于是它自动排除了其他三种可能,完成了类型的收窄。
TypeScript 自定义类型谓词 (is)
typeof、instanceof 和 in 虽然好用,但当业务逻辑变得极其复杂时,我们会习惯性地把判断逻辑抽离成一个独立的函数。这时候,你会遇到一个让无数初学者抓狂的现象。请看下面例子。
示例 5:抽离判断函数导致的 “类型丢失”
interface Fish {
swim(): void;
}
interface Bird {
fly(): void;
}
// 把 “判断逻辑” 抽离成一个函数
function isFish(animal: Fish | Bird): animal is Fish {
// 现代企业级推荐写法:复用 in 关键字,彻底抛弃 as 断言
return "swim" in animal;
}
function move(animal: Fish | Bird) {
if (isFish(animal)) {
// 报错:类型 “Fish | Bird” 上不存在属性 “swim”。
animal.swim();
}
}分析:
为什么会报错呢?这是因为 isFish() 函数的返回值被定死成了普通的 boolean。在 TypeScript 的眼里,它仅仅知道这个函数返回了 true 或 false,但它完全不知道这个返回值和参数 animal 的类型之间有什么必然联系!类型的线索在这里已经被斩断了。
为了解决这个千古难题,TypeScript 引入类型谓词:is 关键字。
语法:
参数名 is 具体类型我们只需要用它来替换掉普通函数返回值处的 boolean 即可。
示例 6:使用 is 激活类型谓词
interface Fish {
swim(): void;
}
interface Bird {
fly(): void;
}
// 将返回值 boolean 改为 animal is Fish
function isFish(animal: Fish | Bird): animal is Fish {
// 现代企业级推荐写法:复用 in 关键字,彻底抛弃 as 断言
return "swim" in animal;
}
function move(animal: Fish | Bird) {
if (isFish(animal)) {
// TypeScript 已经完美识别出 animal 现在就是 Fish
console.log("在水里游泳...");
animal.swim();
} else {
// TypeScript 自动推导 animal 为 Bird
console.log("在天空翱翔...");
animal.fly();
}
}
// ================= 创建对象 =================
// 1. 创建一个实现了 Fish 接口的对象
const myFish: Fish = {
swim() {
console.log("扑通扑通~");
}
};
move(myFish);
// 2. 创建一个实现了 Bird 接口的对象
const myBird: Bird = {
fly() {
console.log("呼啦呼啦~");
}
};
move(myBird);运行结果如下。
在水里游泳...
扑通扑通~
在天空翱翔...
呼啦呼啦~分析:
当我们把返回值写成 animal is Fish 时,其实是在明确告诉 TypeScript 编译器:“只要这个函数返回了 true,传进来的这个 animal 绝对是 Fish 类型,你在后面的代码里放心大胆地收窄类型吧!”
这就是企业级大厂底层源码中最常使用的自定义类型保护机制,它彻底打通了跨函数调用的类型推导壁垒。
类型收窄(Type Narrowing)
在 TypeScript 的官方文档中,我们上面做的这一系列操作(通过 if/else、typeof、is 等手段,把宽泛的 A | B 类型一步步缩小为确定类型的过程),统称为 “类型收窄(Type Narrowing)”。
小伙伴们可以把 “类型收窄” 想象成一个 “漏斗”。一开始流进去的是一个非常宽泛的联合类型(包含了各种可能性)。但在经过层层 if/else 的条件拦截(类型保护)后,最终从漏斗底端流出来的,就是一个非常精确、绝对安全的单一类型了。

掌握类型收窄,也是我们从 TypeScript 新手走向架构师的必经之路。
