文章

类型守卫的实现方式深度解析

类型守卫就是让 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;
    }
  }
}

其实你每天都在用

  1. API 返回处理:if (res.ok) { res.json() } — TS 窄化 Response 类型的子属性
  2. filter(Boolean):配合类型谓词 arr.filter((x): x is NonNullable<T> => x != null) 清空 null
  3. document.querySelector:if (el instanceof HTMLInputElement) { el.value } 安全访问特有属性
  4. React conditional rendering:if (!user) return <Login />; return <Dashboard user={user} /> 后 user 不是 null
  5. 表单事件处理: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 也知道这件事,从此双向安全。

本文由作者按照 CC BY 4.0 进行授权