别再尝试 `typeof x === Fish`:TypeScript 类型验证实用指南(窄化 + 类型谓词)

发布: (2026年1月5日 GMT+8 22:45)
4 min read
原文: Dev.to

Source: Dev.to

Cover image for Stop trying typeof x === Fish: A practical guide to TypeScript type verification (Narrowing + Type Predicates)

当你来自“类型化思维”时,自然会想写类似下面的代码:

typeof animal === Fish

但 JavaScript 并不是这么工作的。

1) 核心思想:类型在运行时不存在

TypeScript 的类型在编译后会被擦除。运行时,你只能得到 JavaScript 的值。
运行时检查通常是下面这些形式:

  • typeof x === "string"
  • x instanceof Date
  • "swim" in animal
  • animal.kind === "fish"

TypeScript 会利用这些检查来 缩窄 联合类型。

2) 缩窄:TypeScript 能理解常见的 JS 模式

typeof 缩窄(原始类型)

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") return " ".repeat(padding) + input;
  return padding + input;
}

instanceof 缩窄(类 / 构造函数)

function logValue(x: Date | string) {
  if (x instanceof Date) return x.toUTCString();
  return x.toUpperCase();
}

"in" 缩窄(属性是否存在)

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ("swim" in animal) animal.swim();
  else animal.fly();
}

注意: in 会检查原型链,且可选属性会影响两个分支。

3) 类型谓词:让缩窄可复用(真正的优势)

当你需要在多个地方复用同一个检查(例如在 .filter() 中),类型谓词(用户自定义类型守卫)就派上用场:

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function isFish(animal: Fish | Bird): animal is Fish {
  return "swim" in animal;
}

function move(animal: Fish | Bird) {
  if (isFish(animal)) animal.swim();
  else animal.fly();
}

现在同一个函数可以被复用:

const zoo: (Fish | Bird)[] = [/* ... */];
const fishes = zoo.filter(isFish); // Fish[]

另一个带有辨别属性的例子:

type ButtonAsLink = { href: string; onClick?: never };
type ButtonAsAction = { onClick: () => void; href?: never };
type Props = { label: string } & (ButtonAsLink | ButtonAsAction);

function isLinkProps(p: Props): p is Props & ButtonAsLink {
  return "href" in p;
}

function SmartButton(props: Props) {
  if (isLinkProps(props)) {
    return {props.label};
  }
  return {props.label};
}

4) 当你能控制模型时的最佳实践:辨别联合

如果可以修改数据结构,使用辨别联合是最稳妥的做法:

type Fish = { kind: "fish"; swim: () => void };
type Bird = { kind: "bird"; fly: () => void };

function move(animal: Fish | Bird) {
  if (animal.kind === "fish") animal.swim();
  else animal.fly();
}

这比属性检查更清晰,并且在联合类型扩展时也更易维护。

5) 常见陷阱(务必记住)

  • typeof null === "object"(历史遗留的 JS 奇怪行为)
  • !value 检查的是 假值0""false),而不仅仅是 null/undefined
  • "prop" in obj 可能因为原型链而为真
  • 可选属性可能导致两个分支仍然包含同一类型(例如 Human 可能有 swim?()

要点

  • 运行时验证 来自 JavaScript 检查。
  • 编译时安全 来自 TypeScript 的缩窄。
  • 当需要复用时,把检查封装进 类型谓词

“使用 JS 检查来缩窄类型,使用类型谓词来复用缩窄。”

Back to Blog

相关文章

阅读更多 »