TypeScript类型守卫怎么写?typeof、in与类型谓词三种缩小方式

编程狮 2026-09-20 10:16:52 浏览数 (13)
反馈

TypeScript 类型守卫通过运行时判断缩小静态类型:基本类型用 typeof,按属性区分对象用 in,复杂外部数据用返回类型谓词的校验函数。

TypeScript 类型守卫封面

你把接口返回值声明成 unknown,访问字段时编译器报错;改成 as User 虽然不报错,运行时却可能读到 undefined。原因是类型断言只改变编译器看法,不验证真实数据。本文以 TypeScript 5.x 通用控制流分析为范围,分别完成基本类型、联合对象和未知 JSON 三个案例。本机没有 tsc,代码未执行,输出标为预期结果。

一、先看结论:三种类型守卫怎么选

场景 推荐写法 是否运行时检查 说明
基本类型或函数 typeof value === "string" 适合 string、number、boolean、bigint、symbol、undefined、function
联合对象有专属属性 "lives" in pet 沿原型链检查属性是否存在
复杂外部数据 function isUser(value: unknown): value is User 是,取决于实现 校验逻辑必须自己写完整
可辨识联合 switch (kind) + kind: "cat" 推荐用于稳定的业务模型
仅缺少类型上下文 as Type 只有拥有额外可靠证据时使用
生命周期保证非空 value! 只告诉编译器忽略 null 与 undefined

一句话:运行时判断缩小静态类型;断言只改变编译器视角,不能替代验证。

二、类型守卫的作用是把大范围缩小成可证明范围

联合类型 string | number 表示值可能属于两个集合。进入 if (typeof value === "string") 分支后,编译器能证明该分支中 valuestring,这个过程叫类型缩小。

function formatId(id: string | number): string {
  // 进入这个分支后,id 被缩小为 string
  if (typeof id === "string") {
    return id.trim().toUpperCase();
  }

  // 剩余分支中,id 被缩小为 number
  return id.toFixed(0);
}

console.log(formatId(" order-7 "));
console.log(formatId(42.4));

预期结果(未在本机执行)为:

ORDER-7
42

typeof 适合 stringnumberbooleanbigintsymbolundefinedfunctiontypeof null 的结果是 object,不能用它区分 null 和普通对象。

typeof 结果 对应类型
"string" string
"number" number
"boolean" boolean
"bigint" bigint
"symbol" symbol
"undefined" undefined
"function" function
"object" null、数组、普通对象等

还不熟悉联合类型和控制流分析时,可以先读 TypeScript 基础教程。类型守卫必须对应真实运行条件,不能只为了让错误提示消失。

三、用 in 区分拥有不同属性的对象

当联合类型的成员有稳定的专属属性,可以使用 in。下面的 CatlivesDogtrained

type Cat = { name: string; lives: number };
type Dog = { name: string; trained: boolean };

function describe(pet: Cat | Dog): string {
  // 如果 pet 拥有 lives 属性,就缩小为 Cat
  if ("lives" in pet) {
    return `${pet.name} has ${pet.lives} lives`;
  }

  // 剩余分支中,pet 被缩小为 Dog
  return `${pet.name} trained=${pet.trained}`;
}

console.log(describe({ name: "Milo", lives: 9 }));
console.log(describe({ name: "Rex", trained: true }));

预期结果(未在本机执行)分别包含:

Milo has 9 lives
Rex trained=true

in 会沿原型链检查属性;处理不可信 JSON 时,通常还要先确认值是非 null 对象,并验证属性值类型。

可选属性会让边界更复杂。如果 CatDog 都可能出现 kind,单靠 in 无法准确区分。业务模型允许时,优先使用可辨识联合:给每个成员增加 kind: "cat"kind: "dog"switch 能同时获得缩小和穷尽检查。

type Cat = { kind: "cat"; name: string; lives: number };
type Dog = { kind: "dog"; name: string; trained: boolean };

function describe(pet: Cat | Dog): string {
  switch (pet.kind) {
    // kind 为 "cat" 时,pet 被缩小为 Cat
    case "cat":
      return `${pet.name} has ${pet.lives} lives`;

    // kind 为 "dog" 时,pet 被缩小为 Dog
    case "dog":
      return `${pet.name} trained=${pet.trained}`;
  }
}
区分方式 适合场景 是否推荐
in 检查专属属性 联合成员属性差异稳定 可用
可辨识联合 + switch 业务模型允许增加 kind 优先推荐
检查多个属性值 结构复杂、属性可选 容易出错,谨慎使用

四、用类型谓词封装复杂的运行时校验

类型谓词写成 value is User,表示函数返回 true 时,调用方可以把 value 缩小为 User。但编译器不会检查函数实现是否足够严格,校验逻辑必须由你负责。

type User = {
  id: number;
  name: string;
  roles: string[];
};

function isUser(value: unknown): value is User {
  // 先排除 null 和非对象
  if (typeof value !== "object" || value === null) return false;

  // 把 value 当作普通对象继续检查字段
  const record = value as Record<string, unknown>;

  // 逐项检查字段类型
  return typeof record.id === "number"
    && typeof record.name === "string"
    && Array.isArray(record.roles)
    && record.roles.every(role => typeof role === "string");
}

function readUser(input: unknown): string {
  // 如果校验不通过,抛出错误
  if (!isUser(input)) {
    throw new Error("invalid user payload");
  }

  // 通过守卫后,input 被缩小为 User
  return `${input.id}:${input.name}:${input.roles.join("|")}`;
}

console.log(readUser({ id: 1, name: "w3cschool", roles: ["editor"] }));

预期输出(未在本机执行)为:

1:w3cschool:editor

roles 中混入数字,isUser 返回 falsereadUser 抛出 invalid user payload。失败路径必须测试,因为一个过宽的类型谓词会让错误数据带着“已验证”的标签进入业务代码。

类型谓词检查项 示例 漏掉后果
排除 null value === null 访问属性时报错
排除非对象 typeof value !== "object" 原始类型进入对象逻辑
检查字段存在 typeof record.id 缺字段时类型不匹配
检查字段类型 typeof record.name === "string" 数字被当成字符串
检查数组成员 record.roles.every(...) 数组内混入错误类型

五、断言、非空断言与类型守卫的边界不同

as User 是类型断言,它不产生运行时代码;value! 是非空断言,只告诉编译器忽略 nullundefined。两者都不能验证接口返回值。

// as 断言:不产生运行时代码,只改变编译器看法
const user = data as User;

// 非空断言:告诉编译器 value 不是 null 或 undefined
const name = value!.name;
写法 是否运行时检查 适用场景
typeof / in 简单联合类型
自定义类型谓词 是,取决于实现 复杂对象、外部输入
as Type 你拥有额外可靠证据时
value! 生命周期保证非空但类型系统无法得知时

处理 DOM、JSON.parsepostMessage 或接口响应时,应从 unknown 开始,再通过守卫缩小。把外部数据直接写成 User 只是跳过检查。

JavaScript 的 typeofArray.isArray 和对象属性规则决定守卫的运行时行为,可以结合 JavaScript 类型基础教程 复习。TypeScript 不会在编译后自动保留接口和类型别名。

六、三种 TypeScript 类型守卫怎么选择

选择顺序可以很简单:

  1. 基本类型或函数:先用 typeof
  2. 联合对象有专属属性:使用 in,或改造成 kind 可辨识联合;
  3. 值来自网络、存储或用户输入:写完整运行时校验,再用类型谓词连接给编译器;
  4. 结构很深或规则会复用:考虑专门的 Schema 校验库,并保留错误路径;
  5. 只有编译器缺少上下文,而你拥有其他可靠证据:才使用断言。

失败排查时,先打印原始 valuetypeof,检查 null、数组与可选字段,再单独测试守卫的 true/false 样例。不要在守卫内部吞掉转换错误,也不要一边校验一边修改输入对象。

用错误样例反证守卫是否可靠

前置条件是把外部输入声明为 unknown,并准备一条完整数据和至少四条失败数据:null、缺少字段、字段类型错误、数组成员类型错误。

测试样例 预期结果
完整数据 isUser 返回 true
null isUser 返回 false
缺少 id isUser 返回 false
id 是字符串 isUser 返回 false
roles 混入数字 isUser 返回 false

逐条调用类型守卫,只有完整数据应返回 true;验证通过后再访问目标属性,编译器不应需要额外 as。这样同时验证运行时判断和静态缩小,而不是只证明返回类型写得漂亮。

若守卫在字段缺失时抛异常,说明访问顺序不安全,应先排除 null 与非对象,再检查属性存在和具体类型。若接口结构经常变化,应采用 Schema 校验库输出精确错误路径,并从同一 Schema 推导 TypeScript 类型,减少手写规则漂移。本文示例按 TypeScript 类型缩小规则静态核对,但当前环境没有 tsc;复制后应运行类型检查和测试,确认正常样例通过、四类错误样例都被拒绝。

提交评审时同时展示守卫函数、输入样例和调用后的缩小结果,能够让审查者确认“检查了什么”与“编译器相信什么”完全一致,避免类型承诺悄悄宽于实际校验。

TypeScript unknown 数据通过三类守卫的缩小流程

总结

TypeScript 类型守卫把运行时事实交给静态类型系统。typeof 解决基本类型,in 解决结构差异,自定义类型谓词负责复杂外部数据;断言只改变编译器视角,不能替代验证。

真正可靠的守卫要有正常样例、缺字段、错类型和 null 四类测试。只要输入越过网络或存储边界,就从 unknown 开始,验证通过后再进入业务逻辑。

延伸学习

  1. 【体系课】前端开发从0基础入门到就业 补齐 JavaScript 与 TypeScript 的运行时基础;
  2. 阅读 TypeScript 7 升级判断笔记,理解编译器版本与项目迁移边界;
  3. 对照 TypeScript 泛型笔记,区分类型参数和运行时校验。

常见问题

Q:类型谓词写对返回类型就一定安全吗?

A:不一定。编译器相信 value is Type 的承诺,却不会证明函数检查完整。守卫实现漏字段或漏类型时,错误数据仍会被当成目标类型。

Q:为什么 typeof nullobject

A:这是 JavaScript 的历史行为。对象守卫应先判断 typeof value === "object",再额外排除 value === null

Q:in 能判断属性值的类型吗?

A:不能。它只判断属性是否存在于对象或原型链。外部输入还要继续检查 typeofArray.isArray 或更具体规则。

Q:什么时候可以使用 as

A:当类型系统缺少信息,但你有来自框架契约、固定 DOM 节点或前置校验的可靠证据时可以使用。对未知接口数据,不应只靠 as

0 人点赞