TypeScript 类型保护

在 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 类型收窄

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

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号