文章

类型守卫的实现方式

类型守卫的实现方式

一句话概括

类型守卫就是你写的 if 判断,TypeScript 编译器能”看懂”——typeof x === "string" 之后,x 在 if 块里就是 string,不用手动 as,也不用担心运行时炸。

核心知识点

1. typeof 守卫——基本类型判断

1
2
3
4
5
6
7
8
function format(x: string | number) {
  if (typeof x === "string") {
    return x.toUpperCase(); // x: string ✅
  }
  return x.toFixed(2); // x: number ✅
}
// 能收窄的类型:string | number | boolean | symbol | undefined | function | bigint
// ⚠️ typeof null === "object",所以 typeof 不能区分 null 和对象

2. instanceof 守卫——类/构造函数判断

1
2
3
4
5
6
7
8
9
10
11
class Dog { bark() {} }
class Cat { meow() {} }

function handle(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark(); // animal: Dog ✅
  } else {
    animal.meow(); // animal: Cat ✅
  }
}
// 本质是检查原型链:new.target.prototype 是否在实例的原型链上

3. in 守卫——属性存在性判断

1
2
3
4
5
6
7
8
9
10
11
interface User { name: string; email: string; }
interface Admin { name: string; permissions: string[]; }

function greet(p: User | Admin) {
  if ("email" in p) {
    console.log(p.email); // p: User ✅
  } else {
    console.log(p.permissions); // p: Admin ✅
  }
}
// 当你不需要创建类、只想通过"有无某属性"区分类型时,in 最方便

4. 自定义类型谓词 is——最灵活的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Fish { swim(): void; }
interface Bird { fly(): void; }

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 ✅
  }
}

类型谓词的超级实用场景——数组过滤

1
2
3
4
5
6
7
8
9
10
11
// ❌ 不用类型谓词:filter 后仍是原类型
const arr = [1, null, 2, undefined, 3];
const filtered = arr.filter(x => x != null);
// filtered: (number | null | undefined)[] ← 类型没变!

// ✅ 用类型谓词:filter 后类型收窄
function isNonNull<T>(x: T | null | undefined): x is T {
  return x != null;
}
const safe = arr.filter(isNonNull);
// safe: number[] ← 收窄了!

5. 可辨识联合——最优雅的模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Result =
  | { kind: "ok"; data: unknown }
  | { kind: "err"; message: string; code: number }
  | { kind: "loading" };

function handle(r: Result) {
  switch (r.kind) {
    case "ok":    return r.data;     // r 收窄到 ok 分支 ✅
    case "err":   return r.message;  // r 收窄到 err 分支 ✅
    case "loading": return "wait…";
    default: {
      const _exhaustive: never = r; // 穷尽检查:漏了分支会报错
      return _exhaustive;
    }
  }
}
// kind 就是"判别属性"(discriminant),每个分支有唯一字面量值

「其实你每天都在用」

  1. API 响应处理if (res.code === 200) 后 data 字段类型自动确定
  2. null 检查if (user) 之后 user 就不是 null 了(严格模式下)
  3. filter(Boolean):配合类型谓词可以写 arr.filter((x): x is NonNullable<T> => x != null)
  4. 表单校验:不同字段类型有不同的值类型,switch 分支自动收窄
  5. React props 类型if ("children" in props) 判断是否传入 children

常见误解(FAQ)

❌ 误区 1:类型守卫会在运行时执行类型检查

类型守卫的语法是编译时作用——它告诉 TS 编译器”这个 if 分支里变量是什么类型”。运行时检查的是你写在守卫函数体里的逻辑(如 typeof x === "string""swim" in pet),而不是类型注解。

❌ 误区 2:instanceof 能检查 interface

不能。instanceof 依赖原型链,interface 是纯类型概念,编译后不存在。要对 interface 做类型守卫,用 in 或自定义类型谓词。

❌ 误区 3:写了 is 类型谓词就不用管函数体对不对了

类型谓词的返回类型声明只是”承诺”——如果函数体逻辑有 bug,TS 不会帮你验证。pet is Fish 如果实际判断逻辑写错了,运行时照样炸。

❌ 误区 4:if (typeof x === "object") 能判断对象

不能区分普通对象和 null,因为 typeof null === "object" 是 JS 的著名遗留 bug。正确写法:if (x !== null && typeof x === "object")

一句话总结

类型守卫的本质是让 TS 编译器”读懂你的 if”——用好它,90% 的 as 断言都可以被消灭,代码安全性和可读性双提升。

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