那天,小区门口贴出一张新通知:从今天起,外来访客分三类——园艺工、维修工、宠物主人。你要坐在登记台前,先问清楚来的是哪一类,再决定放他进哪个门。一开始你也犯难:有些人拎着工具箱,有些人推着婴儿车,光看外表猜不准。后来你练出几套问话技巧——先问“您开的什么车”,再看“您包里装了什么”,拿不准时还能翻一翻门卫处发下来的《访客识别手册》。这套“先确认身份,再放行进小区”的流程,和 TypeScript 里的“类型守卫与断言”做的事一模一样。 TypeScript 像一位严格的门卫:当变量声明为“可能是数字,也可能是字符串”时,它会拦住所有只属于数字的操作。你想让代码顺利跑下去,就得在运行时朝它喊一句“我确认过了”。类型守卫就是那些标准的问话方式,让 TypeScript 在特定代码块里相信你,把联合类型收窄成具体类型。而断言更像是“老住户优先”:你拍胸脯说“他就是维修工”,门卫就放行。可万一认错了人,麻烦都在后头。读完这篇文章,你将摸清每种问话工具的场景,知道什么时候该问、什么时候该放行。

岗位关联

对 AI 从业者来说,模型接口返回的数据常常是“不听话的客人”——join 类型、any、空值满天飞。掌握类型守卫与断言,等于给 AI 产品的前端装上了一个不会漏人的门禁系统,少一堆运行时崩溃,多一份发布时的底气。

目录
  1. 可视化演示
  2. 联合类型收窄
  3. typeof 守卫
  4. instanceof 守卫
  5. in 操作符
  6. is 类型谓词
  7. 断言使用策略
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

步进式展示TypeScript类型守卫的收窄过程:联合类型经过typeof、instanceof、in、is四种守卫逐步收窄为确切类型,对比类型断言as的风险与安全用法。

本节配有交互动效(进入视口后自动加载,约 3KB)。

联合类型起始状态 变量类型为string|number|object联合类型 编译器只知道是三种类型之一,无法安全访问特定属性 type T = string | number | object 联合类型:3种可能类型

联合类型收窄

你在社区入口值了一上午班,手里的访客名单写着“园艺工或维修工”。如果只看名字不抬头确认,你根本不知道该把他领到花园登记处还是工具房。联合类型就是这张模糊的名单:一个变量可能是字符串,也可能是数字,甚至可能是另外几种类型。

TypeScript 用竖线把候选类型拼在一起,比如 string | number。它和“或”一个意思:变量在运行时只能是其中之一,但编译期并不知道具体是哪一个。为了让代码继续写下去,程序必须先做判断,把这个不确定性缩小。在 TypeScript 里,这种候选类型被称作联合类型,编译器会在通过 if/else、switch 等控制流语句的检查后,把变量在某个分支里的类型范围从“所有候选”缩成“一种”,这个过程叫类型收窄(Type Narrowing)。有了收窄,你才能安全调用字符串的 toUpperCase、数字的 toFixed。

所以,登记台工作的第一步永远是抬头问清楚。问过之后,才能选择下一步的工具——typeof、instanceof、in、is、as,本质上都是为了让“收窄”这件事做得又快又稳。

T=T1∪T2∪⋯∪Tn\displaystyle T = T_1 \cup T_2 \cup \cdots \cup T_n
这个公式说:联合类型 T 本质上就是一组候选类型的“并集”,运行时变量只会是其中一种。
  1. T 代表联合类型表示一个变量可能出现的全部类型集合。
    T\displaystyle T
  2. T₁、T₂ 等候选类型构成联合的每一种具体类型,例如字符串、数字、布尔值。
    T1,  T2\displaystyle T_1,\; T_2
  3. ∪ 并集符号表示“或”,变量可以是集合中的任意一员。
    ∪\displaystyle \cup
  4. 等号表明联合类型和候选类型并集是等价的定义关系。
    =\displaystyle =

typeof 守卫

你决定用最省事的一问开张:“您是走着来的,还是开车来的?”这个答案只能分辨大概类别,不涉及具体身份。typeof 就是这样的“基础型问话”:它回答你一个值属于哪种 JavaScript 内置大类。

typeof 是 JS 里早已存在的运算符,TypeScript 把它变成了一个有用的守卫。配合 if 判断,编译器的控制流分析会跟着它走:只要条件成立,分支内变量就被收窄成对应的基础类型。它能识别字符串、数字、布尔、symbol、bigint、undefined、function、object 这几类。注意,它只认“大类”,不管你是普通对象还是数组,只要非空都是 object。最经典的坑是 typeof null 返回 'object'——这是历史遗留问题,导致它不能用来判断空值。

用 typeof 只能把人分到“走路的/开车的”大类。如果访客是维修工或摄影师,你需要像看制服一样,做更精确的检查。

typeof v∈{’string’, ’number’, ’object’, ⋯ }\displaystyle \text{typeof } v \in \{\text{'string'},\, \text{'number'},\, \text{'object'},\, \cdots\}
typeof 的返回值永远是一个描述类型名字的字符串,公式表示它一定落在合法的返回集合里。
  1. typeof 操作符对操作数执行类型查询,返回一个描述类型的字符串。
    typeof\displaystyle \text{typeof}
  2. 操作数 v被检查的变量或表达式,例如一个 string | number 的联合值。
    v\displaystyle v
  3. ∈ 属于符号表示返回结果一定在这些字符串之一,不会跳出集合。
    ∈\displaystyle \in
  4. 字符串集合列举了 'string'、'number'、'object' 等合法返回值。
    {’string’, ’number’, ’object’, ⋯ }\displaystyle \{\text{'string'},\, \text{'number'},\, \text{'object'},\, \cdots\}

instanceof 守卫

你决定看制服。维修工穿蓝工服,宠物医生穿白大褂。instanceof 就是这双眼睛:它判断一个对象是不是某个类“造出来”的实例。

在 TypeScript 中,instanceof 的右侧必须是构造函数或类。如果左侧对象是右侧构造函数的实例,条件为 true,分支内类型就会收窄为对应的类。它适合处理 Dog | Cat 这种类实例联合。原理上,instanceof 沿着对象的原型链往上找,只要找到右侧的 prototype 就返回 true。因为只看原型链,普通对象字面量、interface 定义的类型在运行时并不存在,它们无法通过 instanceof 来收窄。

工牌只能发给有编制的员工,没什么好说的。但如果访客根本不属于任何公司制服阵营,只看衣物还不行,你想起了翻包检查。

xinstanceof⁡C  ⟺  protoChain(x) 包含 C.prototype\displaystyle x \operatorname{instanceof} C \iff \text{protoChain}(x) \text{ 包含 } C.\text{prototype}
公式翻译成人话:x 是 C 的实例,当且仅当 C 的原型对象出现在 x 的原型链上。
  1. x 左侧对象被检查的值,通常来自一个类实例联合类型。
    x\displaystyle x
  2. instanceof 关键字构造函数调用,用来检查原型链关系。
    instanceof⁡\displaystyle \operatorname{instanceof}
  3. C 右侧类必须是构造函数或类,例如 Dog。
    C\displaystyle C
  4. protoChain(x)从 x 出发不断沿原型向上追溯得到的一条链。
    protoChain(x)\displaystyle \text{protoChain}(x)
  5. C.prototype类的原型对象,若在这条链上就成立。
    C.prototype\displaystyle C.\text{prototype}

in 操作符

你问访客:“背包里有没有工具箱?”在不在包里,掏出来一看便知。in 操作符也是这样,它检查的是对象身上有没有某个属性名。

当两个候选类型都是对象字面量,typeof 和 instanceof 都排不上用场,in 就成了好选择:只要属性名命中,分支内部就收窄到拥有该属性的那个类型。把 in 用得更优雅的方式是给每个对象类型都配一个唯一的“名片字段”,比如 type: 'circle'、type: 'square'。这种写法叫可辨识联合(Discriminated Union)。有了这张名片,代码里用一个 if ('radius' in shape) 或 switch (shape.type) 就能精准分流。注意,in 会连原型链上的属性也算进来,判断时要想清楚。

翻包检查已经能覆盖大多数访客,但如果你需要的不是“有没有带包”,而是“这人是不是某个特定住户”,就得靠内部手册了。

’key’in⁡obj  ⟺  obj 上能访问到 key 属性\displaystyle \text{'key'} \operatorname{in} \text{obj} \iff \text{obj 上能访问到 key 属性}
这个公式表示:key 属性在 obj 身上,等价于通过 obj 能访问到 key 这个属性名。
  1. 'key' 属性名要检查的属性名字符串,例如 'radius'。
    ’key’\displaystyle \text{'key'}
  2. in 操作符判断左侧属性名是否存在于右侧对象。
    in⁡\displaystyle \operatorname{in}
  3. obj 对象被检查的对象,通常是联合类型中的某个候选对象。
    obj\displaystyle \text{obj}
  4. 可访问性包括对象自身属性和原型链上继承来的属性。
    obj 上能访问到 key 属性\displaystyle \text{obj 上能访问到 key 属性}

is 类型谓词

保安队长给你发了一本《访客识别手册》:“但凡看到牵着绳子、吐着舌头的,就归类为狗主人。”内置问话毕竟有限,is 就是你自己编写的“手册条款”。

is 出现在自定义类型守卫函数的返回类型里。一个普通函数返回布尔值,只能表达“是/否”;TypeScript 允许你把返回类型写成 x is Dog,意思是:如果函数返回 true,则参数 x 就被收窄为 Dog。最典型的使用场景是过滤数组:pets.filter(isDog) 之后,TypeScript 能自动推断结果是 Dog[],而不是原来的 Pet[],比写无数次手动断言省力得多。

手册不是万能的。当访客特征模糊到连手册都说不清,你只能用最后的经验拍板——这就是断言。

p(x):x is U\displaystyle p(x): x\ \text{is}\ U
这个签名读作:函数 p 接收一个参数 x,它的返回类型不是普通布尔值,而是“x 是 U”这个类型谓词。
  1. p 函数名自定义守卫函数的名称,例如 isDog。
    p\displaystyle p
  2. (x) 参数被检查的联合类型值,例如一只 Pet。
    x\displaystyle x
  3. x is U 谓词声明若函数返回 true,x 的类型就是 U。
    x is U\displaystyle x\ \text{is}\ U
  4. U 具体类型收窄后的目标类型,例如 Dog。
    U\displaystyle U

断言使用策略

很多时候你没时间慢慢问。看到大院门口走来的熟悉面孔,你直接对门卫喊:“这位是维修工,放行!”——这就是断言:告诉 TypeScript,不用查了,我知道他的类型。

断言用 as 关键字书写:x as string,旧代码风格还有 <string>x 的写法。它纯粹是编译期的声明,不会在运行时做任何校验,编译产物里连一个字节都不会为它保留。非空断言 x! 是断言家族里的精简版,专门移除 null 和 undefined。它只是让你在 TypeScript 面前“豁免”空值检查,如果运行时变量真的是 null,照样报错。合理策略是:守卫优先、断言兜底。面对 unknown 或外部 JSON 数据,先用守卫确认,再用断言让类型落地。随意跨类型断言,等于把错误推迟到运行时爆发。

一整天的登记工作结束,你总结出门卫守则:能靠问话(守卫)确认的,绝不靠拍板(断言);必须拍板时,也要拍得有理有据。类型守卫与断言,本质上就是你给不确定的数据发的那张通行证。

x!: T∪{null}⇒T\displaystyle x!:\ T \cup \{\text{null}\} \Rightarrow T
这个式子表示:非空断言把“类型 T 或 null”收窄成“类型 T”,但运行时不会删除任何值。
  1. x 原变量被断言的目标,可能是 T 或 null。
    x\displaystyle x
  2. ! 非空断言符号告诉编译器“它一定不是空值”。
    !\displaystyle !
  3. T ∪ {null}断言的原始类型范围,包含类型 T 与空值。
    T∪{null}\displaystyle T \cup \{\text{null}\}
  4. ⇒ 收窄结果经过非空断言后,类型范围变成 T。
    ⇒T\displaystyle \Rightarrow T

AI 应用场景

模型推理接口的响应解析

阶段:数据接入与预处理

解决的问题:调用模型推理接口时,返回的 JSON 可能是成功结果,也可能是错误信息。传统做法写一堆 if 判断,类型处处是 any,改一个字段要全局搜索;AI 用联合类型加守卫,在编译期就把成功分支和错误分支锁死,运行时错误也能立刻定位。

具体案例:人脸检测接口返回 { code: 0, data: { result: string } } | { code: 1, message: string }。前端拿到响应后,先 if (resp.code === 0) 收窄成成功分支,再读取 resp.data.result,彻底告别“data 可能不存在”的报错。

表单输入的类型安检

阶段:输入校验与 UI 反馈

解决的问题:事件对象 event.target 在 TypeScript 里类型很含糊,可能是任意 HTMLElement。传统做法直接读 value 会编译报错;用 instanceof HTMLInputElement 或 in 检查 value 属性后再取值,把“门卫放行”前置到用户点击之前。

具体案例:AI 对话输入框的 submit 事件中,先用 event.target instanceof HTMLInputElement 收窄,再读取用户消息发送给模型,避免输入框组件被换掉后静默崩溃。

第三方视觉 SDK 的结果落地

阶段:第三方服务集成

解决的问题:很多视觉 SDK 返回 any,传统做法让 any 一路传染,出 bug 难排查;AI 用 unknown 接收返回值,先做类型守卫确认关键属性存在,再用断言收窄成需要的接口类型。

具体案例:接入人体关键点检测 SDK 后,先用 if ('pose' in result) 判断是否存在姿态数据,再用 result as PoseResult 断言落地,让后续画 2D 骨架的代码拥有完整类型提示。

Python 演示

python39 行
from dataclasses import dataclassfrom typing import Union  @dataclassclass Animal:    name: str  @dataclassclass Dog(Animal):    skill: str = "看门"  @dataclassclass Cat(Animal):    skill: str = "抓老鼠"  def is_dog(animal: Animal) -> bool:    return isinstance(animal, Dog)  def is_cat(animal: Animal) -> bool:    return hasattr(animal, "skill") and not isinstance(animal, Dog)  def describe(animal: Union[Dog, Cat, str]) -> str:    if is_dog(animal):        return f"收窄为狗: {animal.name},特长是{animal.skill}"    if is_cat(animal):        return f"收窄为猫: {animal.name},特长是{animal.skill}"    return f"仍是字符串: {animal}"  if __name__ == "__main__":    visitors = [Dog("旺财"), Cat("咪咪"), "普通访客"]    for visitor in visitors:        print(describe(visitor))
目的:用 Python 的运行时类型检查模拟 TypeScript 中 instanceof、in 和自定义类型谓词的收窄逻辑,让读者直观感受“先判断、再操作”的流程。
  1. def is_dog(animal: Animal) -> bool:模拟自定义类型守卫 isDog,返回布尔值表达“是不是狗”。
  2. return isinstance(animal, Dog)Python 版 instanceof,判断实例是否来自 Dog 类。
  3. hasattr(animal, "skill")Python 版 in,检查对象是否拥有 skill 属性。
  4. if is_dog(animal):守卫成立后,分支内把动物按狗来使用。
  5. visitors = [Dog("旺财"), Cat("咪咪"), "普通访客"]三类访客排队接受登记台检查。
  6. print(describe(visitor))模拟 TypeScript 中联合类型在运行时被逐个收窄并输出结果。

常见误区

误区我以为 `typeof null === 'null'`,这样就能用 typeof 判断空值了。结果一查文档居然返回 "object",直接懵了。

正解正确行为是返回 "object",这是 JavaScript 从早期实现保留下来的历史怪癖。判断 null 必须用 x === null。就像登记台上“空身份牌”不会显示“空”字,反而被归类到“物品”大筐里。

为何常见:null 在语义上明明是独立类型,人的直觉会认为 typeof 应该返回 "null",所以几乎每个 TS 新手都会踩一次。

误区我写 `user as Admin` 之后就放心了,以为它会把普通用户对象转换成管理员对象,权限校验也省了。

正解断言是纯编译期语法,编译后完全消失,不会修改运行时值。如果数据真实类型不符,运行时照样报错。就像你拍胸脯告诉门卫“他是维修工”,门卫放行后他干不了维修活,最终出事还得你负责。

为何常见:“断言”这个词听起来像主动执行的操作,让人误以为它和运行时转换、校验有关。

误区我以为 interface 定义的类型也能用 `x instanceof MyInterface` 判断,结果编译直接报错,右侧要求是构造函数。

正解instanceof 右侧必须是构造函数或类,而 interface 是纯静态类型,编译后不存在。普通对象字面量没有原型链上的类关系,也无法用 instanceof 收窄。制服只发给有编制的公司员工,没做工牌的角色自然无法识别。

为何常见:TypeScript 里“一切皆类型”的错觉,让初学者把静态类型和运行时对象混为一谈。

误区我写了 `user!.name`,以为就算 user 真的是 null,程序也会跳过空值继续运行,结果照样崩溃。

正解x! 只是编译期告诉 TypeScript“这里不会为空”,运行时一个字节都不会少执行。如果 user 确实是 null,读取 name 依然抛出 TypeError。你把“空牌位”放进了贵宾通道,但到了贵宾室还是没有人对应。

为何常见:把编译期的类型收窄当成了运行时保护,这是“一切为了通过编译器”的应试式习惯造成的。

面试题

Q1. typeof 最适合用于哪种联合类型的收窄?

单选题 · easy

  • A.基础类型联合← 正确答案
  • B.类实例联合类型
  • C.两个对象字面量
  • D.任意 interface

答案:A。typeof 只回答“值属于哪种基础大类”的问题,能分辨 string、number 等基础类型。它看不了“公司工牌”,所以类实例联合和 interface 都轮不到它。就像登记台问“走路还是开车”,只能把人大致分流。

Q2. 下列哪个操作符能检查对象是否拥有某个属性?

单选题 · easy

  • A.typeof 查基础类型
  • B.in 查属性存在← 正确答案
  • C.instanceof 查实例
  • D.as 查断言标记

答案:B。in 操作符专门翻包检查——看属性名在不在对象身上。typeof 查基础类型,instanceof 查原型链,as 是断言,都不是属性存在性判断。这道题对应登记台的“翻包检查”环节。

Q3. typeof null 的返回值是什么?

单选题 · easy

  • A.typeof 返回 "null"
  • B.typeof 返回 "object"← 正确答案
  • C.typeof 返回 "undefined"
  • D.typeof 返回 "boolean"

答案:B。JavaScript 历史遗留导致 typeof null 返回 “object“,不是 ”null“。所以判断 null 要用 x === null,不能依赖 typeof。登记台上”空身份牌”被默认归进了物品筐,这是身份系统自己的历史问题。

Q4. 以下场景中,最适合用 instanceof 收窄的是哪个?

单选题 · mid

  • A.string 或 number
  • B.Dog 或 Cat 类实例← 正确答案
  • C.两个对象字面量
  • D.null 联合 undefined

答案:B。instanceof 看“工牌”——判断对象是不是某个类造出来的实例,所以类实例联合最合适。字符串、对象字面量、null 都没有类原型关系,instanceof 管不了。它只对有编制的员工有效。

Q5. 自定义类型守卫函数的正确返回类型写法是什么?

单选题 · hard

  • A.布尔返回类型
  • B.x is Dog 谓词← 正确答案
  • C.Dog 类型本身
  • D.as Dog 断言

答案:B。自定义守卫返回类型要写成 x is Dog,表示“返回 true 时 x 被收窄为 Dog”。普通布尔只能表达真假,不能参与类型收窄。就像手册条款先写“什么特征”,再写“归为哪类人”。

Q6. 可辨识联合通常用哪个字段充当区分类型的“名片”?

单选题 · mid

  • A.name 字段
  • B.type 字段← 正确答案
  • C.value 字段
  • D.key 字段

答案:B。可辨识联合的惯例是用一个字面量字段(常见名为 type 或 kind)当名片,比如 type: 'circle' 与 type: 'square',代码用 switch (shape.type) 就能精准分流。门卫靠名片,而不是靠访客自己报名字。

Q7. 下列写法中,不属于类型断言的是哪一个?

单选题 · extreme

  • A.as 关键字写法
  • B.尖括号写法
  • C.冒号注解写法← 正确答案
  • D.非空断言写法

答案:C。x as string、<string>x、x! 都是断言,它们会改变 TypeScript 对当前表达式的类型理解。而 x: string 是类型注解,用于声明变量类型,不是断言。注解写在变量声明处,断言写在表达式中,两者门卫职责不同。

Q8. 以下哪些属于 TypeScript 的内置类型守卫?

多选题 · mid

  • A.typeof 类型判断← 正确答案
  • B.in 属性判断← 正确答案
  • C.instanceof 实例判断← 正确答案
  • D.as 类型断言

答案:ABC。typeof、instanceof、in 都是内置守卫,靠真实运行时信息收窄类型。as 是断言,不需要任何运行时信息,只靠开发者声明。守卫是“问话后再放行”,断言是“直接拍板”,两者本质不同。

Q9. 关于非空断言 x!,下列说法正确的有哪些?

多选题 · mid

  • A.运行时会删除空值
  • B.编译期声明值非空← 正确答案
  • C.值运行时空值仍在← 正确答案
  • D.可用于表达式内部← 正确答案

答案:BCD。x! 只在编译期告诉 TypeScript“这里非空”,运行时空值依然存在,如果真的是 null 照样抛错。它可以写在表达式里,比如 user!.name。它只是门卫在登记表上盖了一个“已确认”章,不代表访客真的来了。

Q10. 下列哪些做法存在滥用断言的风险?

多选题 · hard

  • A.任意跨类断言← 正确答案
  • B.断言后不加校验← 正确答案
  • C.绕过类型守卫← 正确答案
  • D.守卫后局部断言

答案:ABC。跨类断言、断言后无校验、绕过守卫直接断言,都会让类型声明与真实数据脱节,把错误推迟到运行时。守卫后局部断言是“先问话再拍板”,相对安全。门卫可以拍板,但不能连问都不问就放陌生人进金库。