Kodokon kodokon.com

类型收窄:typeof、in、instanceof 与类型守卫

你将掌握类型收窄的各种机制,并编写自己的谓词,让联合类型更加可靠。

10 分钟 · 3 题

在 Kodokon 中打开本课

类型收窄(Narrowing)是 TypeScript 通过分析控制流,将一个宽泛的类型(联合类型、unknown)细化为更精确类型的机制。你其实一直在不知不觉中使用它:每一个作用于已类型化值的 if 都会触发一次分析。编译器会跟踪三类内置守卫:用于原始类型的 typeof、用于类实例的 instanceof,以及用于判断属性存在与否的 in。恰当地选用它们,能让你免于使用 as 的鲁莽类型断言,因为断言是关闭校验,而不是引导校验。

TYPESCRIPT
function formatValue(
  value: string | number | Date
): string {
  if (typeof value === "string") {
    return value.trim();
  }
  if (value instanceof Date) {
    return value.toISOString();
  }
  return value.toFixed(2);
}
每个分支都收窄了类型;最后一个分支只能是 number。

当你处理结构不同、又没有区分字段的对象时,in 运算符极为宝贵。但要小心:in 测试的是某个键是否存在(包括继承而来的键),而不是它的类型。对于外部数据(API、JSON),它是根据你的声明来收窄,而不是根据运行时实际存在的内容。一个经典的现实陷阱:typeof value === "object" 的检查会放过 null,因为在 JavaScript 中 typeof null 就是 "object"。请始终额外加上明确的 null 检查。

TYPESCRIPT
interface Cat { meow(): void }
interface Dog { bark(): void }

function speak(animal: Cat | Dog): void {
  if ("meow" in animal) {
    animal.meow();
  } else {
    animal.bark();
  }
}
用 in 运算符通过属性是否存在来收窄类型。

当守卫逻辑变得可复用,或过于复杂而超出流程分析的能力时,就编写一个自定义类型守卫:一个返回类型标注为 value is T 的函数。编译器随后会信任你的实现。这是一份契约:如果你的检查不完整,就会重新引入类型系统永远看不到的运行时错误。自 TypeScript 5.5 起,简单谓词会在诸如 filter 的回调中被自动推断,但对于导出的函数而言,显式标注仍是惯例。

TYPESCRIPT
interface User { id: string; email: string }

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    typeof value.id === "string" &&
    "email" in value &&
    typeof value.email === "string"
  );
}

const raw: unknown =
  JSON.parse('{"id":"1","email":"a@b.c"}');
if (isUser(raw)) {
  console.log(raw.email.toLowerCase());
}
一个在使用前先校验外部数据的谓词。

知识检测

确认你已牢记本课的重点内容。

  1. 为什么 typeof value === "object" 的检查不足以收窄为非 null 对象?
    • 因为 typeof 返回的是首字母大写的 "Object"
    • 因为在 JavaScript 中 typeof null 也是 "object"
    • 因为 typeof 对联合类型不起作用
    • 因为数组会返回 "array"
  2. 哪种返回类型标注能把一个函数变成自定义类型守卫?
    • boolean
    • value as User
    • value is User
    • asserts value
  3. 写得不好的自定义类型守卫的主要风险是什么?
    • 函数内部会出现编译期错误
    • 一次撒谎的收窄:编译器相信了一个在运行时错误的类型
    • 编译性能的下降