那天,小区门口贴出一张新通知:从今天起,外来访客分三类——园艺工、维修工、宠物主人。你要坐在登记台前,先问清楚来的是哪一类,再决定放他进哪个门。一开始你也犯难:有些人拎着工具箱,有些人推着婴儿车,光看外表猜不准。后来你练出几套问话技巧——先问“您开的什么车”,再看“您包里装了什么”,拿不准时还能翻一翻门卫处发下来的《访客识别手册》。这套“先确认身份,再放行进小区”的流程,和 TypeScript 里的“类型守卫与断言”做的事一模一样。 TypeScript 像一位严格的门卫:当变量声明为“可能是数字,也可能是字符串”时,它会拦住所有只属于数字的操作。你想让代码顺利跑下去,就得在运行时朝它喊一句“我确认过了”。类型守卫就是那些标准的问话方式,让 TypeScript 在特定代码块里相信你,把联合类型收窄成具体类型。而断言更像是“老住户优先”:你拍胸脯说“他就是维修工”,门卫就放行。可万一认错了人,麻烦都在后头。读完这篇文章,你将摸清每种问话工具的场景,知道什么时候该问、什么时候该放行。
对 AI 从业者来说,模型接口返回的数据常常是“不听话的客人”——join 类型、any、空值满天飞。掌握类型守卫与断言,等于给 AI 产品的前端装上了一个不会漏人的门禁系统,少一堆运行时崩溃,多一份发布时的底气。
可视化演示
步进式展示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 代表联合类型表示一个变量可能出现的全部类型集合。
- T₁、T₂ 等候选类型构成联合的每一种具体类型,例如字符串、数字、布尔值。
- ∪ 并集符号表示“或”,变量可以是集合中的任意一员。
- 等号表明联合类型和候选类型并集是等价的定义关系。
typeof 守卫
你决定用最省事的一问开张:“您是走着来的,还是开车来的?”这个答案只能分辨大概类别,不涉及具体身份。typeof 就是这样的“基础型问话”:它回答你一个值属于哪种 JavaScript 内置大类。
typeof 是 JS 里早已存在的运算符,TypeScript 把它变成了一个有用的守卫。配合 if 判断,编译器的控制流分析会跟着它走:只要条件成立,分支内变量就被收窄成对应的基础类型。它能识别字符串、数字、布尔、symbol、bigint、undefined、function、object 这几类。注意,它只认“大类”,不管你是普通对象还是数组,只要非空都是 object。最经典的坑是 typeof null 返回 'object'——这是历史遗留问题,导致它不能用来判断空值。
用 typeof 只能把人分到“走路的/开车的”大类。如果访客是维修工或摄影师,你需要像看制服一样,做更精确的检查。
- typeof 操作符对操作数执行类型查询,返回一个描述类型的字符串。
- 操作数 v被检查的变量或表达式,例如一个 string | number 的联合值。
- ∈ 属于符号表示返回结果一定在这些字符串之一,不会跳出集合。
- 字符串集合列举了 'string'、'number'、'object' 等合法返回值。
instanceof 守卫
你决定看制服。维修工穿蓝工服,宠物医生穿白大褂。instanceof 就是这双眼睛:它判断一个对象是不是某个类“造出来”的实例。
在 TypeScript 中,instanceof 的右侧必须是构造函数或类。如果左侧对象是右侧构造函数的实例,条件为 true,分支内类型就会收窄为对应的类。它适合处理 Dog | Cat 这种类实例联合。原理上,instanceof 沿着对象的原型链往上找,只要找到右侧的 prototype 就返回 true。因为只看原型链,普通对象字面量、interface 定义的类型在运行时并不存在,它们无法通过 instanceof 来收窄。
工牌只能发给有编制的员工,没什么好说的。但如果访客根本不属于任何公司制服阵营,只看衣物还不行,你想起了翻包检查。
- x 左侧对象被检查的值,通常来自一个类实例联合类型。
- instanceof 关键字构造函数调用,用来检查原型链关系。
- C 右侧类必须是构造函数或类,例如 Dog。
- protoChain(x)从 x 出发不断沿原型向上追溯得到的一条链。
- C.prototype类的原型对象,若在这条链上就成立。
in 操作符
你问访客:“背包里有没有工具箱?”在不在包里,掏出来一看便知。in 操作符也是这样,它检查的是对象身上有没有某个属性名。
当两个候选类型都是对象字面量,typeof 和 instanceof 都排不上用场,in 就成了好选择:只要属性名命中,分支内部就收窄到拥有该属性的那个类型。把 in 用得更优雅的方式是给每个对象类型都配一个唯一的“名片字段”,比如 type: 'circle'、type: 'square'。这种写法叫可辨识联合(Discriminated Union)。有了这张名片,代码里用一个 if ('radius' in shape) 或 switch (shape.type) 就能精准分流。注意,in 会连原型链上的属性也算进来,判断时要想清楚。
翻包检查已经能覆盖大多数访客,但如果你需要的不是“有没有带包”,而是“这人是不是某个特定住户”,就得靠内部手册了。
- 'key' 属性名要检查的属性名字符串,例如 'radius'。
- in 操作符判断左侧属性名是否存在于右侧对象。
- obj 对象被检查的对象,通常是联合类型中的某个候选对象。
- 可访问性包括对象自身属性和原型链上继承来的属性。
is 类型谓词
保安队长给你发了一本《访客识别手册》:“但凡看到牵着绳子、吐着舌头的,就归类为狗主人。”内置问话毕竟有限,is 就是你自己编写的“手册条款”。
is 出现在自定义类型守卫函数的返回类型里。一个普通函数返回布尔值,只能表达“是/否”;TypeScript 允许你把返回类型写成 x is Dog,意思是:如果函数返回 true,则参数 x 就被收窄为 Dog。最典型的使用场景是过滤数组:pets.filter(isDog) 之后,TypeScript 能自动推断结果是 Dog[],而不是原来的 Pet[],比写无数次手动断言省力得多。
手册不是万能的。当访客特征模糊到连手册都说不清,你只能用最后的经验拍板——这就是断言。
- p 函数名自定义守卫函数的名称,例如 isDog。
- (x) 参数被检查的联合类型值,例如一只 Pet。
- x is U 谓词声明若函数返回 true,x 的类型就是 U。
- U 具体类型收窄后的目标类型,例如 Dog。
断言使用策略
很多时候你没时间慢慢问。看到大院门口走来的熟悉面孔,你直接对门卫喊:“这位是维修工,放行!”——这就是断言:告诉 TypeScript,不用查了,我知道他的类型。
断言用 as 关键字书写:x as string,旧代码风格还有 <string>x 的写法。它纯粹是编译期的声明,不会在运行时做任何校验,编译产物里连一个字节都不会为它保留。非空断言 x! 是断言家族里的精简版,专门移除 null 和 undefined。它只是让你在 TypeScript 面前“豁免”空值检查,如果运行时变量真的是 null,照样报错。合理策略是:守卫优先、断言兜底。面对 unknown 或外部 JSON 数据,先用守卫确认,再用断言让类型落地。随意跨类型断言,等于把错误推迟到运行时爆发。
一整天的登记工作结束,你总结出门卫守则:能靠问话(守卫)确认的,绝不靠拍板(断言);必须拍板时,也要拍得有理有据。类型守卫与断言,本质上就是你给不确定的数据发的那张通行证。
- x 原变量被断言的目标,可能是 T 或 null。
- ! 非空断言符号告诉编译器“它一定不是空值”。
- T ∪ {null}断言的原始类型范围,包含类型 T 与空值。
- ⇒ 收窄结果经过非空断言后,类型范围变成 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 演示
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))
def is_dog(animal: Animal) -> bool:模拟自定义类型守卫 isDog,返回布尔值表达“是不是狗”。return isinstance(animal, Dog)Python 版 instanceof,判断实例是否来自 Dog 类。hasattr(animal, "skill")Python 版 in,检查对象是否拥有 skill 属性。if is_dog(animal):守卫成立后,分支内把动物按狗来使用。visitors = [Dog("旺财"), Cat("咪咪"), "普通访客"]三类访客排队接受登记台检查。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 最适合用于哪种联合类型的收窄?
- A.基础类型联合← 正确答案
- B.类实例联合类型
- C.两个对象字面量
- D.任意 interface
答案:A。typeof 只回答“值属于哪种基础大类”的问题,能分辨 string、number 等基础类型。它看不了“公司工牌”,所以类实例联合和 interface 都轮不到它。就像登记台问“走路还是开车”,只能把人大致分流。
Q2. 下列哪个操作符能检查对象是否拥有某个属性?
- A.typeof 查基础类型
- B.in 查属性存在← 正确答案
- C.instanceof 查实例
- D.as 查断言标记
答案:B。in 操作符专门翻包检查——看属性名在不在对象身上。typeof 查基础类型,instanceof 查原型链,as 是断言,都不是属性存在性判断。这道题对应登记台的“翻包检查”环节。
Q3. typeof null 的返回值是什么?
- A.typeof 返回 "null"
- B.typeof 返回 "object"← 正确答案
- C.typeof 返回 "undefined"
- D.typeof 返回 "boolean"
答案:B。JavaScript 历史遗留导致 typeof null 返回 “object“,不是 ”null“。所以判断 null 要用 x === null,不能依赖 typeof。登记台上”空身份牌”被默认归进了物品筐,这是身份系统自己的历史问题。
Q4. 以下场景中,最适合用 instanceof 收窄的是哪个?
- A.string 或 number
- B.Dog 或 Cat 类实例← 正确答案
- C.两个对象字面量
- D.null 联合 undefined
答案:B。instanceof 看“工牌”——判断对象是不是某个类造出来的实例,所以类实例联合最合适。字符串、对象字面量、null 都没有类原型关系,instanceof 管不了。它只对有编制的员工有效。
Q5. 自定义类型守卫函数的正确返回类型写法是什么?
- A.布尔返回类型
- B.x is Dog 谓词← 正确答案
- C.Dog 类型本身
- D.as Dog 断言
答案:B。自定义守卫返回类型要写成 x is Dog,表示“返回 true 时 x 被收窄为 Dog”。普通布尔只能表达真假,不能参与类型收窄。就像手册条款先写“什么特征”,再写“归为哪类人”。
Q6. 可辨识联合通常用哪个字段充当区分类型的“名片”?
- A.name 字段
- B.type 字段← 正确答案
- C.value 字段
- D.key 字段
答案:B。可辨识联合的惯例是用一个字面量字段(常见名为 type 或 kind)当名片,比如 type: 'circle' 与 type: 'square',代码用 switch (shape.type) 就能精准分流。门卫靠名片,而不是靠访客自己报名字。
Q7. 下列写法中,不属于类型断言的是哪一个?
- A.as 关键字写法
- B.尖括号写法
- C.冒号注解写法← 正确答案
- D.非空断言写法
答案:C。x as string、<string>x、x! 都是断言,它们会改变 TypeScript 对当前表达式的类型理解。而 x: string 是类型注解,用于声明变量类型,不是断言。注解写在变量声明处,断言写在表达式中,两者门卫职责不同。
Q8. 以下哪些属于 TypeScript 的内置类型守卫?
- A.typeof 类型判断← 正确答案
- B.in 属性判断← 正确答案
- C.instanceof 实例判断← 正确答案
- D.as 类型断言
答案:ABC。typeof、instanceof、in 都是内置守卫,靠真实运行时信息收窄类型。as 是断言,不需要任何运行时信息,只靠开发者声明。守卫是“问话后再放行”,断言是“直接拍板”,两者本质不同。
Q9. 关于非空断言 x!,下列说法正确的有哪些?
- A.运行时会删除空值
- B.编译期声明值非空← 正确答案
- C.值运行时空值仍在← 正确答案
- D.可用于表达式内部← 正确答案
答案:BCD。x! 只在编译期告诉 TypeScript“这里非空”,运行时空值依然存在,如果真的是 null 照样抛错。它可以写在表达式里,比如 user!.name。它只是门卫在登记表上盖了一个“已确认”章,不代表访客真的来了。
Q10. 下列哪些做法存在滥用断言的风险?
- A.任意跨类断言← 正确答案
- B.断言后不加校验← 正确答案
- C.绕过类型守卫← 正确答案
- D.守卫后局部断言
答案:ABC。跨类断言、断言后无校验、绕过守卫直接断言,都会让类型声明与真实数据脱节,把错误推迟到运行时。守卫后局部断言是“先问话再拍板”,相对安全。门卫可以拍板,但不能连问都不问就放陌生人进金库。