类型守卫的实现方式深度解析
类型守卫就是让 TS 编译器'读懂你的 if'——typeof x === 'string' 之后,x 在 if 块里自动收窄为 string,无需手动 as。 用好它能消灭九成类型断言,安全与可读性兼得。
一句话概括
类型守卫就是让 TS 编译器”读懂你的 if”——typeof x === "string" 之后,x 在 if 块里就是 string,不用手动 as。用好它能消灭 90% 的类型断言,代码安全性和可读性同时拉满。
核心知识点
1. typeof 守卫 —— 基本类型收窄
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function format(input: string | number): string {
if (typeof input === "string") {
return input.toUpperCase(); // input: string ✅
}
return input.toFixed(2); // input: number ✅(TS 自动排除 string)
}
// typeof 能收窄的类型:
// "string" | "number" | "boolean" | "symbol" | "undefined" | "function" | "bigint"
// ⚠️ 经典坑:typeof null === "object",所以 typeof 不能区分 null 和普通对象
function process(val: object | null) {
if (typeof val === "object") {
// val 仍是 object | null —— TS 知道这个坑,不会帮你窄化!
}
// 正确做法:
if (val !== null && typeof val === "object") {
// val: object ✅
}
}
2. instanceof 守卫 —— 类(构造函数)判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ApiError extends Error {
constructor(message: string, public code: number) {
super(message);
}
}
function handleError(err: Error) {
if (err instanceof ApiError) {
console.error(`[${err.code}] ${err.message}`); // err: ApiError ✅
// 可以安全访问 err.code
} else {
console.error(err.message); // err: Error ✅
}
}
instanceof 检查的是原型链——ApiError.prototype 是否在 err 的原型链上。interface 不能用 instanceof,因为编译后不存在。
3. in 守卫 —— 属性存在性判断
1
2
3
4
5
6
7
8
9
10
interface User { name: string; email: string; }
interface Admin { name: string; permissions: string[]; }
function describe(person: User | Admin): string {
if ("email" in person) {
return `用户: ${person.email}`; // person: User ✅
}
return `管理员: ${person.permissions}`; // person: Admin ✅
}
// in 适合不需要创建类、单纯靠"有没有某个属性"区分类型的场景
4. 自定义类型谓词(is)—— 最灵活也最强大
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Fish { swim(): void; }
interface Bird { fly(): void; }
// 返回类型 pet is Fish 就是告诉 TS:"如果这个函数返回 true,则 pet 是 Fish"
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // pet: Fish ✅
} else {
pet.fly(); // pet: Bird ✅
}
}
面试高频应用:用 is 收窄 Array.filter 结果
1
2
3
4
5
6
7
8
9
10
11
// ❌ 不用类型谓词:filter 后类型不变
const arr = [1, null, 2, undefined, 3];
const basic = arr.filter(x => x != null);
// basic: (number | null | undefined)[] ← 类型没变!!
// ✅ 用 is 谓词:filter 后类型自动收窄
function isNonNull<T>(x: T | null | undefined): x is T {
return x != null;
}
const safe = arr.filter(isNonNull);
// safe: number[] ← 收窄了!!
5. 可辨识联合(Discriminated Union)—— 最优雅的模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type NetworkState =
| { status: "idle" }
| { status: "loading"; progress: number }
| { status: "success"; data: unknown }
| { status: "error"; error: Error };
function render(state: NetworkState): string {
switch (state.status) {
case "idle":
return "空闲";
case "loading":
return `加载中 ${state.progress}%`; // state.progress 可用 ✅
case "success":
return `加载完成: ${JSON.stringify(state.data)}`;
case "error":
return `错误: ${state.error.message}`;
default: {
// 穷尽检查——如果新增状态忘了加 case,这里会报错
const _exhaustive: never = state;
return _exhaustive;
}
}
}
其实你每天都在用
- API 返回处理:
if (res.ok) { res.json() }— TS 窄化 Response 类型的子属性 filter(Boolean):配合类型谓词arr.filter((x): x is NonNullable<T> => x != null)清空 nulldocument.querySelector:if (el instanceof HTMLInputElement) { el.value }安全访问特有属性- React conditional rendering:
if (!user) return <Login />; return <Dashboard user={user} />后 user 不是 null - 表单事件处理:
if (e.target instanceof HTMLInputElement)后安全读取e.target.value
常见误解(FAQ)
❌ 误区 1:「写了 is 类型谓词,TS 会验证函数体逻辑是否正确」
不会。 pet is Fish 只是你给编译器的”承诺”——如果你的判断逻辑写错了(比如把所有东西都 return true),TS 不会报错,运行时照样炸。类型谓词的函数体逻辑责任在你。
❌ 误区 2:「instanceof 可以检查 interface」
不能。instanceof 依赖原型链(检查 RightSide.prototype 是否在左边实例的原型链上),interface 是纯类型概念,编译后不存在。对 interface 做类型守卫用 in 或自定义 is。
❌ 误区 3:「if (x) 对所有类型都有效」
if (x) 只能排除 null 和 undefined(以及 0、""、false、NaN)。对于 string | number 这种联合,if (x) 不会收窄,因为 TS 不知道你想排除哪个。
❌ 误区 4:「类型守卫只在 TS 里有用,写 JS 没用」
正确,类型守卫是纯编译时概念。但 typeof、instanceof、in 这些运行时检查本身就是有效的 JS 防御性代码——类型守卫只是额外让 TS 编译器理解了这个防御逻辑的语义。
一句话总结
类型守卫的本质不是”改变运行时行为”,而是让编译器理解你已有的防御逻辑——你本来就要写 if (typeof x === "string"),加上类型守卫不过是让 TS 也知道这件事,从此双向安全。