TypeScript 类型守卫通过运行时判断缩小静态类型:基本类型用 typeof,按属性区分对象用 in,复杂外部数据用返回类型谓词的校验函数。

你把接口返回值声明成 unknown,访问字段时编译器报错;改成 as User 虽然不报错,运行时却可能读到 undefined。原因是类型断言只改变编译器看法,不验证真实数据。本文以 TypeScript 5.x 通用控制流分析为范围,分别完成基本类型、联合对象和未知 JSON 三个案例。本机没有 tsc,代码未执行,输出标为预期结果。
一、先看结论:三种类型守卫怎么选
| 场景 | 推荐写法 | 是否运行时检查 | 说明 |
|---|---|---|---|
| 基本类型或函数 | typeof value === "string" |
是 | 适合 string、number、boolean、bigint、symbol、undefined、function |
| 联合对象有专属属性 | "lives" in pet |
是 | 沿原型链检查属性是否存在 |
| 复杂外部数据 | function isUser(value: unknown): value is User |
是,取决于实现 | 校验逻辑必须自己写完整 |
| 可辨识联合 | switch (kind) + kind: "cat" |
是 | 推荐用于稳定的业务模型 |
| 仅缺少类型上下文 | as Type |
否 | 只有拥有额外可靠证据时使用 |
| 生命周期保证非空 | value! |
否 | 只告诉编译器忽略 null 与 undefined |
一句话:运行时判断缩小静态类型;断言只改变编译器视角,不能替代验证。
二、类型守卫的作用是把大范围缩小成可证明范围
联合类型 string | number 表示值可能属于两个集合。进入 if (typeof value === "string") 分支后,编译器能证明该分支中 value 是 string,这个过程叫类型缩小。
function formatId(id: string | number): string {
// 进入这个分支后,id 被缩小为 string
if (typeof id === "string") {
return id.trim().toUpperCase();
}
// 剩余分支中,id 被缩小为 number
return id.toFixed(0);
}
console.log(formatId(" order-7 "));
console.log(formatId(42.4));
预期结果(未在本机执行)为:
ORDER-7
42
typeof 适合 string、number、boolean、bigint、symbol、undefined 与 function;typeof null 的结果是 object,不能用它区分 null 和普通对象。
typeof 结果 |
对应类型 |
|---|---|
"string" |
string |
"number" |
number |
"boolean" |
boolean |
"bigint" |
bigint |
"symbol" |
symbol |
"undefined" |
undefined |
"function" |
function |
"object" |
null、数组、普通对象等 |
还不熟悉联合类型和控制流分析时,可以先读 TypeScript 基础教程。类型守卫必须对应真实运行条件,不能只为了让错误提示消失。
三、用 in 区分拥有不同属性的对象
当联合类型的成员有稳定的专属属性,可以使用 in。下面的 Cat 有 lives,Dog 有 trained:
type Cat = { name: string; lives: number };
type Dog = { name: string; trained: boolean };
function describe(pet: Cat | Dog): string {
// 如果 pet 拥有 lives 属性,就缩小为 Cat
if ("lives" in pet) {
return `${pet.name} has ${pet.lives} lives`;
}
// 剩余分支中,pet 被缩小为 Dog
return `${pet.name} trained=${pet.trained}`;
}
console.log(describe({ name: "Milo", lives: 9 }));
console.log(describe({ name: "Rex", trained: true }));
预期结果(未在本机执行)分别包含:
Milo has 9 lives
Rex trained=true
in 会沿原型链检查属性;处理不可信 JSON 时,通常还要先确认值是非 null 对象,并验证属性值类型。
可选属性会让边界更复杂。如果 Cat 和 Dog 都可能出现 kind,单靠 in 无法准确区分。业务模型允许时,优先使用可辨识联合:给每个成员增加 kind: "cat" 或 kind: "dog",switch 能同时获得缩小和穷尽检查。
type Cat = { kind: "cat"; name: string; lives: number };
type Dog = { kind: "dog"; name: string; trained: boolean };
function describe(pet: Cat | Dog): string {
switch (pet.kind) {
// kind 为 "cat" 时,pet 被缩小为 Cat
case "cat":
return `${pet.name} has ${pet.lives} lives`;
// kind 为 "dog" 时,pet 被缩小为 Dog
case "dog":
return `${pet.name} trained=${pet.trained}`;
}
}
| 区分方式 | 适合场景 | 是否推荐 |
|---|---|---|
in 检查专属属性 |
联合成员属性差异稳定 | 可用 |
可辨识联合 + switch |
业务模型允许增加 kind |
优先推荐 |
| 检查多个属性值 | 结构复杂、属性可选 | 容易出错,谨慎使用 |
四、用类型谓词封装复杂的运行时校验
类型谓词写成 value is User,表示函数返回 true 时,调用方可以把 value 缩小为 User。但编译器不会检查函数实现是否足够严格,校验逻辑必须由你负责。
type User = {
id: number;
name: string;
roles: string[];
};
function isUser(value: unknown): value is User {
// 先排除 null 和非对象
if (typeof value !== "object" || value === null) return false;
// 把 value 当作普通对象继续检查字段
const record = value as Record<string, unknown>;
// 逐项检查字段类型
return typeof record.id === "number"
&& typeof record.name === "string"
&& Array.isArray(record.roles)
&& record.roles.every(role => typeof role === "string");
}
function readUser(input: unknown): string {
// 如果校验不通过,抛出错误
if (!isUser(input)) {
throw new Error("invalid user payload");
}
// 通过守卫后,input 被缩小为 User
return `${input.id}:${input.name}:${input.roles.join("|")}`;
}
console.log(readUser({ id: 1, name: "w3cschool", roles: ["editor"] }));
预期输出(未在本机执行)为:
1:w3cschool:editor
若 roles 中混入数字,isUser 返回 false,readUser 抛出 invalid user payload。失败路径必须测试,因为一个过宽的类型谓词会让错误数据带着“已验证”的标签进入业务代码。
| 类型谓词检查项 | 示例 | 漏掉后果 |
|---|---|---|
| 排除 null | value === null |
访问属性时报错 |
| 排除非对象 | typeof value !== "object" |
原始类型进入对象逻辑 |
| 检查字段存在 | typeof record.id |
缺字段时类型不匹配 |
| 检查字段类型 | typeof record.name === "string" |
数字被当成字符串 |
| 检查数组成员 | record.roles.every(...) |
数组内混入错误类型 |
五、断言、非空断言与类型守卫的边界不同
as User 是类型断言,它不产生运行时代码;value! 是非空断言,只告诉编译器忽略 null 与 undefined。两者都不能验证接口返回值。
// as 断言:不产生运行时代码,只改变编译器看法
const user = data as User;
// 非空断言:告诉编译器 value 不是 null 或 undefined
const name = value!.name;
| 写法 | 是否运行时检查 | 适用场景 |
|---|---|---|
typeof / in |
是 | 简单联合类型 |
| 自定义类型谓词 | 是,取决于实现 | 复杂对象、外部输入 |
as Type |
否 | 你拥有额外可靠证据时 |
value! |
否 | 生命周期保证非空但类型系统无法得知时 |
处理 DOM、JSON.parse、postMessage 或接口响应时,应从 unknown 开始,再通过守卫缩小。把外部数据直接写成 User 只是跳过检查。
JavaScript 的 typeof、Array.isArray 和对象属性规则决定守卫的运行时行为,可以结合 JavaScript 类型基础教程 复习。TypeScript 不会在编译后自动保留接口和类型别名。
六、三种 TypeScript 类型守卫怎么选择
选择顺序可以很简单:
- 基本类型或函数:先用
typeof; - 联合对象有专属属性:使用
in,或改造成kind可辨识联合; - 值来自网络、存储或用户输入:写完整运行时校验,再用类型谓词连接给编译器;
- 结构很深或规则会复用:考虑专门的 Schema 校验库,并保留错误路径;
- 只有编译器缺少上下文,而你拥有其他可靠证据:才使用断言。
失败排查时,先打印原始 value 和 typeof,检查 null、数组与可选字段,再单独测试守卫的 true/false 样例。不要在守卫内部吞掉转换错误,也不要一边校验一边修改输入对象。
用错误样例反证守卫是否可靠
前置条件是把外部输入声明为 unknown,并准备一条完整数据和至少四条失败数据:null、缺少字段、字段类型错误、数组成员类型错误。
| 测试样例 | 预期结果 |
|---|---|
| 完整数据 | isUser 返回 true |
null |
isUser 返回 false |
缺少 id |
isUser 返回 false |
id 是字符串 |
isUser 返回 false |
roles 混入数字 |
isUser 返回 false |
逐条调用类型守卫,只有完整数据应返回 true;验证通过后再访问目标属性,编译器不应需要额外 as。这样同时验证运行时判断和静态缩小,而不是只证明返回类型写得漂亮。
若守卫在字段缺失时抛异常,说明访问顺序不安全,应先排除 null 与非对象,再检查属性存在和具体类型。若接口结构经常变化,应采用 Schema 校验库输出精确错误路径,并从同一 Schema 推导 TypeScript 类型,减少手写规则漂移。本文示例按 TypeScript 类型缩小规则静态核对,但当前环境没有 tsc;复制后应运行类型检查和测试,确认正常样例通过、四类错误样例都被拒绝。
提交评审时同时展示守卫函数、输入样例和调用后的缩小结果,能够让审查者确认“检查了什么”与“编译器相信什么”完全一致,避免类型承诺悄悄宽于实际校验。

总结
TypeScript 类型守卫把运行时事实交给静态类型系统。typeof 解决基本类型,in 解决结构差异,自定义类型谓词负责复杂外部数据;断言只改变编译器视角,不能替代验证。
真正可靠的守卫要有正常样例、缺字段、错类型和 null 四类测试。只要输入越过网络或存储边界,就从 unknown 开始,验证通过后再进入业务逻辑。
延伸学习
- 用 【体系课】前端开发从0基础入门到就业 补齐 JavaScript 与 TypeScript 的运行时基础;
- 阅读 TypeScript 7 升级判断笔记,理解编译器版本与项目迁移边界;
- 对照 TypeScript 泛型笔记,区分类型参数和运行时校验。
常见问题
Q:类型谓词写对返回类型就一定安全吗?
A:不一定。编译器相信 value is Type 的承诺,却不会证明函数检查完整。守卫实现漏字段或漏类型时,错误数据仍会被当成目标类型。
Q:为什么 typeof null 是 object?
A:这是 JavaScript 的历史行为。对象守卫应先判断 typeof value === "object",再额外排除 value === null。
Q:in 能判断属性值的类型吗?
A:不能。它只判断属性是否存在于对象或原型链。外部输入还要继续检查 typeof、Array.isArray 或更具体规则。
Q:什么时候可以使用 as?
A:当类型系统缺少信息,但你有来自框架契约、固定 DOM 节点或前置校验的可靠证据时可以使用。对未知接口数据,不应只靠 as。

免费 AI IDE



