试想一位老侦探接到报案。他没有证人,没看监控,只沿着现场走了一圈:桌上的咖啡还没凉、门锁没有撬痕、后窗的脚印是 42 码——他心里已经有了嫌疑人的画像。TypeScript 的编译器就是这样的侦探。你写下 let age = 30,它不需要你多解释,自动明白 age 是个数字;你写 if (typeof name === 'string'),它在花括号里自动把 name 当作字符串对待。前者叫类型推断——侦探根据现场痕迹勾勒画像;后者叫类型收窄——侦探根据逐条线索划掉一批批嫌疑人,直到锁定真凶。这篇教程带你翻开这位侦探的办案手册:看他如何推断、如何收窄、如何用判别联合与类型守卫织起一张精密的安全网,以及他何时会坦诚地说:“这里,我需要你亲手标注。”

岗位关联

对 AI 从业者而言,类型推断与收窄是理解 TypeScript 类型系统的钥匙:它让你写的每行代码都有据可查、有错可抓,也让 AI 辅助生成的代码天然更安全。掌握它,你与编译器、与 AI 工具的协作效率都会上一个台阶。

目录
  1. 可视化演示
  2. 推断机制基础
  3. 上下文类型推演
  4. 基础收窄手段
  5. 操作符收窄术
  6. 判别联合类型
  7. 类型守卫实践
  8. 推断边界管理
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

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

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

推断机制基础

侦探到案发现场,一眼扫过:桌上的外卖单、茶几上的药瓶、沙发上的猫毛。他还没问任何人,心里已经拼出屋主的大致画像:独居、有猫、作息不规律。TypeScript 也一样——你写 let age = 30,它连问都不问,就知道 age 是数字。这就是推断的魔力:不需要你开口,它自己看。

这个“看一眼就知道”的能力叫类型推断(type inference)。TypeScript 依据变量的初始化值、函数的返回语句、参数默认值等可见信息,自动推导出类型。它省去了大量手动标注,让你把精力放在业务逻辑上。

推断有几条硬规则。第一,初始化值决定类型:let x = 42 推断为 number,let greeting = 'hi' 推断为 string。第二,const 与 let 待遇不同:const 声明后不可重新赋值,编译器直接采用字面量类型;let 允许日后改值,编译器会拓宽为基本类型。第三,函数返回值从 return 语句推断:先把参数类型标清楚,返回值的类型就基本不用操心了。

const  x=42  ⇒  x:42let  y=42  ⇒  y:number\displaystyle \texttt{const}\; x = 42 \;\Rightarrow\; x : 42 \qquad \texttt{let}\; y = 42 \;\Rightarrow\; y : \texttt{number}

1. const 字面量直记:const 声明后不可重新绑定,TypeScript 把值本身记作类型,42 被推断为字面量类型 42。

const  x=42⇒x:42\texttt{const}\; x = 42 \Rightarrow x : 42

2. let 拓宽为基类:let 允许日后重新赋值,编译器自动把 42 拓宽为 number,为 y = 99 这类操作预留空间。

let  y=42⇒y:number\texttt{let}\; y = 42 \Rightarrow y : \texttt{number}

3. 从右向左流向:类型信息沿等号从右侧初始值流向左侧变量,这是最基础的推断路径。

42⇒x42 \Rightarrow x

就像侦探先画“初步画像”,TypeScript 用最省力的方式给你第一版类型。画像未必精确,但已经能拦下最离谱的错误——比如给 age 传字符串,侦探一眼看出“这不对”。

上下文类型推演

侦探不光看现场,还看“情境”:深夜出现在高档小区的年轻人,和白天出现在菜市场的年轻人,调查方向完全不同。类型推断也有类似的“情境智慧”——当代码处在一个期待某种类型的场景里,类型信息会从场景反向流进代码。这就是上下文类型(contextual typing)。

最常见的例子是数组 map 的回调:arr.map(item => item.length) 中,item 不需要任何标注,TypeScript 自动知道它是数组元素类型。事件监听更典型:button.addEventListener('click', e => ...) 里的 e 自动是 MouseEvent,你在 IDE 里敲 e.,clientX、target 全都弹出来。

上下文类型的本质是“期望类型反推实际类型”。数组声明了元素类型,map 的签名就要求回调参数匹配该类型;回调里的 item 于是被反推出来。对象字面量赋值给接口时也同样生效——甚至反过来做“多余属性检查”,你多写了接口没声明的属性,它会当场报警。

arr.map((item)⇒item.length)⇒item:T\displaystyle \texttt{arr.map}((\texttt{item}) \Rightarrow \texttt{item.length}) \quad\Rightarrow\quad \texttt{item}: T

1. map 签名固定:Array<T>.map 规定回调首参数类型为 T,T 由数组元素类型决定。

map:((item:T)⇒U)⇒U[]\texttt{map}: ((item: T) \Rightarrow U) \Rightarrow U[]

2. 期望类型反向流入:TypeScript 把 T 代入回调的可见位置,item 自动获得类型,无需手写 (item: string)。

T⇒itemT \Rightarrow \texttt{item}

3. 双向汇合:正向推断给出 T,反向推断给出 item 类型,两股信息流在表达式中央汇合。

T正向∩item反向T_{\text{正向}} \cap \texttt{item}_{\text{反向}}

侦探利用“情境”反推嫌疑人画像;TypeScript 利用“使用场景”反推变量类型。你少写标注,它多干活——但前提是场景本身类型清晰,否则就退回上节的“初步画像”了。

基础收窄手段

侦探有了初步画像,但嫌疑人太多。这时每条新线索都能划掉一批人:“目击者说凶手是左撇子”划掉右撇子,“凶手穿 42 码鞋”再划掉一批。TypeScript 在代码分支里也在做同样的事,这叫类型收窄(narrowing)。

当编译器执行控制流分析(control flow analysis)时,它会顺着 if、else、switch、逻辑表达式的路径,记录下“在这一行,这个变量还可能是哪些类型”。最常见的触发者是 typeof 检查:if (typeof x === 'string') 之后的花括号里,x 被收窄为 string,你可以放心调用字符串方法。

收窄的细节值得留意。真值收窄是隐藏的帮手:if (x) 会把 null、undefined、0、''、NaN 全部排除,剩下的分支里 x 必然是“有值”的类型。switch 同样厉害:每个 case 分支独立收窄,case 'circle' 里知道是圆、case 'square' 里知道是方,互不干扰。

if  (typeof  x  ===  "string")  ⇒  x:string(else⇒x:number)\displaystyle \texttt{if}\;(\texttt{typeof}\; x \;\texttt{===}\; \texttt{"string"}) \;\Rightarrow\; x : \texttt{string} \qquad (\texttt{else} \Rightarrow x : \texttt{number})

1. typeof 运行时探测:typeof 返回 'string'、'number'、'boolean'、'object' 等类型名字符串。

typeof  x⇒"string"  ∣  "number"\texttt{typeof}\; x \Rightarrow \texttt{"string"} \;|\; \texttt{"number"}

2. 等式触发收窄:把 typeof 的结果与字面量 'string' 比较,编译器在 true 分支中知道 x 只可能是 string。

typeof  x  ===  "string"⇒x:string\texttt{typeof}\; x \;\texttt{===}\; \texttt{"string"} \Rightarrow x : \texttt{string}

3. else 分支互补:未被收窄的类型自动落入 else,两边恰好覆盖整个联合类型。

¬(typeof  x  ===  "string")⇒x:number\neg(\texttt{typeof}\; x \;\texttt{===}\; \texttt{"string"}) \Rightarrow x : \texttt{number}

每一条 if 就是一条目击证词,帮侦探划掉一批嫌疑人。而编译器最擅长“记住你检查过什么”——出了 if 分支,它立刻恢复警惕,这正好引出下一节“收窄的纪律”。

操作符收窄术

仅有目击证词还不够,侦探还有三样专业工具:核对指纹库(instanceof)、翻查随身物品清单(in)、比对身高体貌(等式比较)。三样工具各有适用场景,TypeScript 的收窄工具箱里也备着它们。

instanceof 负责“这是不是某个类的实例”:x instanceof Date 成立时,x 从 Date | string 收窄为 Date,getTime()、toISOString() 都能直接调用。in 操作符负责“属性是否存在”:'radius' in s 成立时,联合类型中只有带 radius 字段的成员幸免。等式收窄则处理字面量:x === 'success' 把 'success' | 'error' 联合定位到 'success'。

这些工具也有失效场景。控制流分析只在“当前作用域、没有未知调用”的路径上有效。变量一旦穿过函数调用边界——比如被 setTimeout 回调捕获、被传给另一个函数——编译器无法知道函数何时执行、变量是否已被改动,于是选择“失忆”,把类型恢复为收窄前的联合类型。

x  instanceof  Date  ⇒  x:Date"radius"  in  s  ⇒  s:{radius}\displaystyle x \;\texttt{instanceof}\; \texttt{Date} \;\Rightarrow\; x : \texttt{Date} \qquad \texttt{"radius"} \;\texttt{in}\; s \;\Rightarrow\; s : \{ \texttt{radius} \}

1. instanceof 检查原型链:instanceof 验证对象原型链上是否存在目标构造函数的原型,编译器信赖该检查,把 x 收窄为 Date。

x  instanceof  Date⇒x:Datex \;\texttt{instanceof}\; \texttt{Date} \Rightarrow x : \texttt{Date}

2. in 检查属性存在:'radius' in s 说明对象携带 radius 字段,联合类型中只有 Circle 具备它。

"radius"  in  s⇒s:Circle\texttt{"radius"} \;\texttt{in}\; s \Rightarrow s : \texttt{Circle}

3. 作用域边界重置:这些收窄只对“已经发生的检查”生效,一旦跨越函数调用,编译器认定变量可能被改动而重置。

call(x)⇒x:原始联合类型\texttt{call}(x) \Rightarrow x : \texttt{原始联合类型}

侦探不会把指纹比对的结果带出实验室还当铁证——他没盯着的证据,随时可能变。TypeScript 跨过函数边界就“失忆”,这不是笨拙,而是严谨:它承认自己无法确定。

判别联合类型

侦探最爱的破案神器是一套“分类档案”:每份档案封面都印着醒目的类别标签——“盗窃”、“诈骗”、“抢劫”。看一眼标签,后面几十页不用翻。TypeScript 的判别联合类型(discriminated union)就是这套档案系统。

定义一个联合类型时,给每个成员一个同名字段(惯例叫 kind 或 type),字段值是互不相同的字面量类型。编译器只要看到这个判别字段的比较,就能自动把变量收窄到对应成员:

联合类型表达“可能是其中之一”,判别字段负责“是哪一 个”。最妙的是穷尽性检查:在 switch 的 default 分支里把变量赋给 never,如果哪天你新增了一个成员却忘了处理,default 里的类型不再是 never,编译器立刻报错——侦探的档案系统拒绝“未分类卷宗”。

if  (s.kind  ≡  "circle")  ⇒  s:Circleelse⇒s:Square\displaystyle \texttt{if}\; (s.\texttt{kind} \;\equiv\; \texttt{"circle"}) \;\Rightarrow\; s : \texttt{Circle} \qquad \texttt{else} \Rightarrow s : \texttt{Square}

1. 判别字段唯一标识:kind 是每个成员独有的常量值,等价于档案封面上的唯一标签。

s.kind:"circle"  ∣  "square"s.\texttt{kind} : \texttt{"circle"} \;|\; \texttt{"square"}

2. 联合的并集语义:Shape 可取任一成员,判别字段比较把候选集合收缩到唯一成员。

s∈{Circle,Square}∧s.kind="circle"⇒s=Circles \in \{\texttt{Circle}, \texttt{Square}\} \wedge s.\texttt{kind} = \texttt{"circle"} \Rightarrow s = \texttt{Circle}

3. 分支自动收窄:编译器把字面量 'circle' 与成员的类型比对,等于时锁定 Circle 并暴露 radius 字段。

s:Circle⇒s.radius:numbers : \texttt{Circle} \Rightarrow s.\texttt{radius} : \texttt{number}

加了判别字段的联合类型,是 TypeScript 中最优雅的收窄体验——侦探只看标签,编译器只看 kind,代码从“层层排查”升级为“对号入座”。

ts8 行
type Shape =  | { kind: 'circle'; radius: number }  | { kind: 'square'; width: number }; function area(s: Shape) {  if (s.kind === 'circle') return Math.PI * s.radius ** 2;  return s.width ** 2;}

类型守卫实践

标准探案流程有时不够用:指纹有重叠、目击者有口音、档案标签被撕。老侦探会写一本“私人排查手册”,把复杂场所的检查流程归纳成固定步骤。TypeScript 里这就是类型守卫(type guard)——一个返回谓词签名的函数:

谓词签名 s is Circle 是 TS 的独门语法:它声明“当函数返回 true 时,参数 s 就是 Circle”。只要函数返回 true,调用处的分支自动收窄——和内置的 typeof、instanceof 效果一样。区别在于:守卫是可跨函数复用的移动排查包。

守卫可以组合使用:一个守卫内部可以调用其他守卫,比如先 isShape 再 isCircle;业务代码里写 if (isCircle(s) && isSquare(t)) 也能同时收窄两个变量。唯一要注意的是实现约束:编译器不会检查函数体是否真的正确推断——它信赖你的声明。所以守卫的实现要尽可能简单、可测试,像侦探手册里的每一条规则都必须经过实战检验。

isCircle(s):  s  is  Circle  ⟺  s.kind  ≡  "circle"\displaystyle \texttt{isCircle}(s) :\; s \;\texttt{is}\; \texttt{Circle} \;\Longleftrightarrow\; s.\texttt{kind} \;\equiv\; \texttt{"circle"}

1. 参数位置绑定:谓词签名必须针对函数参数(这里是 s),不能针对任意表达式。

isCircle(s:Shape):s  is  Circle\texttt{isCircle}(s: \texttt{Shape}) : s \;\texttt{is}\; \texttt{Circle}

2. true 绑定收窄:返回 true 时参数收窄为 Circle;返回 false 时排除 Circle,余下成员进入 else。

isCircle(s)=true⇒s:Circle\texttt{isCircle}(s) = \texttt{true} \Rightarrow s : \texttt{Circle}

3. 实现由人保证:函数体是普通代码,编译器不校验实现与谓词是否一致,靠开发者自律。

(isCircle(s)⇒s.kind)  需人工验证(\texttt{isCircle}(s) \Rightarrow s.\texttt{kind}) \;\text{需人工验证}

类型守卫让排查能力随身携带。但注意:手册是人写的,写错了编译器也拦不住——守卫的实现要简单、可测试,这正是“理论与实践”在侦探世界的分界线。

ts8 行
function isCircle(s: Shape): s is Circle {  return s.kind === 'circle';} if (isCircle(s)) {  // 这里 s 自动收窄为 Circle  return Math.PI * s.radius ** 2;}

推断边界管理

再厉害的侦探也有盲区:现场被清理过、目击者消失、时间跨度太大。这时他会拿起电话调动正式警力资源,而不是硬猜。TypeScript 也清楚地知道自己的边界,并且会坦诚地告诉你:“这里,我需要你亲手标注。”

推断的局限在几个地方尤其明显。第一,函数参数:参数没有初始值,正向推断和反向推断都失效,必须手动标注。第二,跨模块与递归类型:type Node = { value: number; children?: Node[] } 这类自引用结构,推断难以兜底。第三,复杂泛型:嵌套的工具类型、条件类型,编译器推导结果往往超出人类可读范围。

手动标注的时机有一个朴素原则:局部变量靠推断,对外契约靠标注。函数签名、接口、API 边界都必须写清楚——它们是团队协作的契约书。同时要警惕两个陷阱:any 会关闭该处的类型检查,错误全部延迟到运行时;类型断言 as 是“我说了算”的捷径,as any 更是核弹级操作,只应在 JSON.parse、跨库边界等有明确运行时证据的场景使用。

function  add(a,b)  ⇒  a,b:需手动标注as  string:跳过类型检查的危险捷径\displaystyle \texttt{function}\; \texttt{add}(a, b) \;\Rightarrow\; a, b : \text{需手动标注} \qquad \texttt{as}\; \texttt{string} : \text{跳过类型检查的危险捷径}

1. 参数无源可推:函数参数没有初始化值,正向与反向推断同时失效,必须标注。

add(a,b)⇒a:unknown⇒标注必要性=1\texttt{add}(a, b) \Rightarrow a : \texttt{unknown} \Rightarrow \text{标注必要性} = 1

2. 契约优先原则:对外导出的函数像警方的对外承诺,类型必须白纸黑字写明。

add(a:number,b:number):number\texttt{add}(a: \texttt{number}, b: \texttt{number}) : \texttt{number}

3. 断言是例外:as 绕过类型检查,相当于侦探跳过取证直接定案,只能用于少数有把握的场景。

any⇒类型检查关闭as⇒开发者声明优先\texttt{any} \Rightarrow \text{类型检查关闭} \qquad \texttt{as} \Rightarrow \text{开发者声明优先}

侦探的边界感成就了他的可靠——知道何时该申请搜查令(标注)、何时凭经验速断(推断),案件才办得又稳又快。TypeScript 开发者同理:把推断留给局部,把标注留给边界。

AI 应用场景

AI 辅助生成表单状态渲染

阶段:代码生成与类型安全

解决的问题:传统手写表单页需要大量 if/else 判断加载态、成功态、失败态,漏判一个分支页面就白屏。用判别联合类型建模状态后,AI 生成的渲染代码每写一个分支,编译器都帮忙验证是否访问了正确的字段。

具体案例:电商结算页的“提交订单”按钮有三种状态:加载中、成功、失败。定义 type SubmitState = { status: 'loading' } | { status: 'success'; orderId: string } | { status: 'error'; message: string },提示词要求 AI 用 switch (state.status) 渲染,生成的代码天然不会把 orderId 用到错误分支上。

API 响应解析的防御层

阶段:数据处理与边界防御

解决的问题:后端接口返回“成功数据或错误对象”时,手写解析常忘判空导致运行时崩溃。用类型守卫封装判断逻辑,AI 生成的调用代码自动走收窄分支,错误被挡在数据层之外。

具体案例:订单查询接口返回 { data: Order } | { error: { code: number; msg: string } }。定义一个 isOrderResponse(resp) 守卫(内部用 'data' in resp 判断),AI 辅助生成的业务代码只需在守卫为真时访问 resp.data,TypeScript 自动收窄,从根源杜绝漏判。

大型 JS 项目迁移 TS 的类型补齐

阶段:代码迁移与静态分析

解决的问题:老项目数百个模块从 JS 迁 TS,逐个手写标注不现实。AI 工具基于控制流分析自动推断、补写标注,对推断不了的边界(如动态全局变量)提示开发者手动声明。

具体案例:迁移支付模块时,AI 分析 const status = order.paid ? 'paid' : 'pending',自动推断 status 为字面量联合 'paid' | 'pending',后续的 switch 收窄全部自动生效;遇到 window.__config 这类动态全局变量,则提示开发者显式声明接口类型。

Python 演示

python57 行
"""迷你版 TS 推断与收窄演示:模拟 typeof 探测与判别联合收窄."""from math import pifrom typing import Union  class Circle:    kind = "circle"     def __init__(self, radius):        self.radius = radius  class Square:    kind = "square"     def __init__(self, width):        self.width = width  Shape = Union[Circle, Square]  def narrow(shape: Shape) -> float:    """按 kind 判别字段收窄,相当于 TS 的 discriminated union."""    if shape.kind == "circle":        return pi * shape.radius ** 2    if shape.kind == "square":        return shape.width ** 2    raise ValueError("unknown shape kind")  def kind_of(value) -> str:    """模拟 typeof 收窄:先排除 None,再按类型逐一试探."""    if value is None:        return "null"    if isinstance(value, bool):        return "boolean"    if isinstance(value, (int, float)):        return "number"    if isinstance(value, str):        return "string"    return "object"  def infer_pair(a, b):    """简单推断:两个同类型值相加,返回类型标签."""    ta, tb = kind_of(a), kind_of(b)    if ta != tb:        raise TypeError("type mismatch, cannot add")    return ta  if __name__ == "__main__":    shapes = [Circle(2), Square(3)]    for s in shapes:        print(kind_of(s), "->", narrow(s))    print("infer add:", infer_pair(1, 2), infer_pair("a", "b"))
目的:用 Python 复刻 TypeScript 的两大核心机制——typeof 式类型探测与判别联合收窄,帮你直观理解编译器“看值定类型”的思路。
  1. Shape = Union[Circle, Square]定义判别联合类型,对应 TS 的 type Shape = Circle | Square
  2. if shape.kind == "circle":按 kind 判别字段收窄,只有圆才访问 radius
  3. raise ValueError("unknown shape kind")模拟 TS 的 never 穷尽检查,未知分支直接报错
  4. if value is None:先排除 None,对应 TS 中先处理 null 再判断对象的防御习惯
  5. isinstance(value, (int, float))模拟 typeof 探测 number 类型
  6. if __name__ == "__main__":可执行入口,演示两种 shape 的面积计算

常见误区

误区刚学 TS 时觉得这工具真聪明,变量、参数、返回值都不用写类型了,它自己都会推。函数参数也懒得标,反正编译过了就行。

正解推断只覆盖“有可见信息”的场景:初始化值、返回值、默认值。函数参数没有初始值,不标注就是 any(开启 strict 后直接报错)。侦探再厉害,没人告诉他嫌疑人的身高,他也画不出画像。正确策略是:局部变量靠推断,函数签名、接口、API 边界必须手动标注。

为何常见:TS 宣传语强调“减少标注”,入门 demo 在关闭 noImplicitAny 时省略参数也能跑,容易让人误以为推断万能。

误区想判断一个值是不是对象就写 `if (typeof v === 'object')`,结果 `null` 也溜进来了,程序报错时一脸懵:typeof 不是能探测类型吗?

正解typeof null === 'object' 是 JS 从诞生起就存在的设计缺陷,TS 的收窄逻辑继承了这个事实。判断对象前必须先排除 null,或者写成 v && typeof v === 'object'。侦探不能只靠“身上有口袋”这条线索抓人——口袋人人都有,还得结合身高(等式检查)、指纹(instanceof)等多条线索交叉验证。

为何常见:教程讲 typeof 多用 string/number 例子,很少主动提 null 陷阱;加上 'object' 一词本身极具误导性。

误区编译器报错就 `as any` 或 `as Foo`,世界立刻安静。久而久之养成习惯:报错?断言!收窄不了?断言!

正解类型断言是“我比编译器更懂”的声明,本质是绕过检查。as any 直接让该变量脱离类型系统,等于侦探把卷宗烧了凭感觉办案,错误全留到运行时爆炸。断言应在有明确运行时证据时使用(如 JSON.parse 的返回、跨库边界),且优先用 unknown 中转,而不是 any。

为何常见:断言写法简单、报错消失极快,带来即时的“解决感”,容易形成路径依赖。

误区在函数开头明明 `if (typeof x === 'string')` 了,后面调 `setTimeout`、`forEach` 时又想直接用字符串方法,结果类型又变回联合,报错说“可能不是 string”。

正解控制流分析只对“当前作用域、没有未知调用”的路径有效。变量一旦被闭包捕获,TS 无法知道回调何时执行,会保留进入闭包前的类型。侦探把线索交给搭档去查,搭档的调查范围必然重新扩大——他不在现场,证据可能已经变了。

为何常见:直觉认为“检查过就一直安全”,忽略了 JS 动态执行与函数调用是黑盒的事实。

误区看教程都是 `switch (shape.kind)`,以为只有 switch 能触发 discriminated union 的收窄,在 if 里写 `shape.kind === 'circle'` 担心不生效。

正解只要在控制流中比较判别字段,if、三元表达式、提前 return 都能触发自动收窄,switch 只是最常见写法。侦探查档案编号,用电子检索还是手工翻卡片都一样,关键在“核对了唯一编号”。

为何常见:教材示例偏好 switch 的整齐结构,让读者误以为它是必需语法。

面试题

Q1. 下列哪种写法能被 TypeScript 自动推断为 number 类型的变量?

单选题 · easy

  • A.let a = "hello" 字符串
  • B.let b = 42 数字值← 正确答案
  • C.let c = true 布尔值
  • D.let d = Symbol() 符号

答案:B。TypeScript 根据初始化值推断类型,"hello" 是字符串、true 是布尔、Symbol() 是符号,只有 42 是数字。这就像侦探看现场留下的脚印就能判断鞋码——值本身就是最直接的线索。

Q2. 哪个运算符能在运行时获取变量的类型信息?

单选题 · easy

  • A.typeof 能获取类型的名字← 正确答案
  • B.in 能检查属性是否存在
  • C.new 能创建对象新实例
  • D.as 能执行类型断言操作

答案:A。typeof 是 JS 运行时运算符,返回 'string'、'number' 等类型名字符串;in 检查属性、new 创建实例、as 做类型断言,都与“运行时探测类型”无关。侦探要判断嫌疑人身份,第一步就是查验证件——typeof 就是那张证件。

Q3. const name = "Alice" 中 name 的类型是什么?

单选题 · mid

  • A.string 也就是字符串类型
  • B."Alice" 字面量类型别名← 正确答案
  • C.any 表示任意类型均可
  • D.unknown 未知类型保险柜

答案:B。const 声明不可重新赋值,TS 把初始值记为字面量类型 "Alice";let 才会拓宽为 string。any 和 unknown 需要显式标注才会出现。画像越具体,后面排查越省力——侦探记下“身高 170cm”比“中等身材”有用得多。

Q4. 判别联合类型要触发自动收窄,最关键的前提是什么?

单选题 · mid

  • A.成员都带 kind 判别字段← 正确答案
  • B.成员都继承同一个父类
  • C.联合类型成员数量为二
  • D.判别字段类型是 string

答案:A。判别字段(如 kind)在每个成员中都存在且值唯一,编译器才能把检查结果对号入座到唯一成员。继承父类不产生收窄依据,成员数量、字段类型(字面量联合也可)都不是关键。档案没有唯一编号,侦探就无法快速锁定。

Q5. 类型守卫函数的谓词签名正确写法是?

单选题 · mid

  • A.function isC(s: Shape): boolean
  • B.function isC(s: Shape): s is Circle← 正确答案
  • C.function isC(s: Shape): Circle
  • D.function isC(s: Shape): true

答案:B。谓词签名写作 s is Circle,声明“函数返回 true 时参数 s 被收窄为 Circle”。返回 boolean 只表达是或否,不携带类型信息;返回 Circle 是“构造”而非“判断”。错误的签名无法在调用处触发自动收窄,等于侦探手册写错了规则。

Q6. 数组 map 回调参数 item 的类型从何而来?

单选题 · mid

  • A.上下文类型推断自动获得← 正确答案
  • B.必须手动显式标注获得
  • C.运行时读取数组元素获得
  • D.由类型断言强制获得

答案:A。map 的回调参数由上下文类型推断自动获得数组元素类型,无需手动标注。这是“期望类型反向流动”的典型例子——数组声明了元素类型,map 的签名把类型反推给回调参数,像侦探根据现场定位反推嫌疑人活动范围。

Q7. 变量在 if 分支中被收窄为 string,传入 setTimeout 回调后为何又恢复为原来的联合类型?

单选题 · hard

  • A.回调可能在任何时刻执行← 正确答案
  • B.编译器的收窄出现漏洞
  • C.字符串类型被自动拓宽
  • D.回调内类型重新推断错

答案:A。闭包可能在变量被重新赋值后的任意时刻执行,TS 无法追踪回调内部的变量状态,因此在回调内收窄被重置。这不是 bug,而是编译器严谨地承认“我不能确定”。侦探把线索交给搭档后,搭档的调查范围必然重新扩大。

Q8. 关于类型收窄的基础手段,正确的说法有?

多选题 · easy

  • A.typeof 可探测基本类型← 正确答案
  • B.if (x) 能排除空值← 正确答案
  • C.switch 分支可独立收窄← 正确答案
  • D.typeof 可探测所有类型

答案:ABC。typeof 探测基本类型、真值检查排除空值、switch 各分支独立收窄都是基础手段。但 typeof 无法区分所有类型——typeof null 也返回 'object'。侦探的每一条线索都有适用范围,没有万能工具。

Q9. 关于上下文类型推断,正确的说法有?

多选题 · mid

  • A.回调参数自动获得类型← 正确答案
  • B.事件对象推断为具体事件← 正确答案
  • C.期望类型反向流向参数← 正确答案
  • D.推断不适用于 map 回调

答案:ABC。上下文类型推断让回调参数、事件对象自动获得预期类型,期望类型从使用场景反向流入参数——这正是 map、事件监听等场景无需标注的原因。D 与事实完全相反,map 是上下文推断最典型的例子。

Q10. 关于 never 类型与穷尽性检查,下列说法正确的有?

多选题 · extreme

  • A.never 可以赋值给任何类型← 正确答案
  • B.穷尽检查将变量变为 never← 正确答案
  • C.never 是 null 类型的别名
  • D.never 适合表示永不返回← 正确答案

答案:ABD。never 可赋给任何类型、穷尽检查把 default 分支的变量收窄为 never、never 表示永不返回的函数,三者都正确。never 与 null 完全不同:null 是有值(空值),never 是“不可能”,两者不可混为一谈——侦探会说“没有嫌疑人”和“凶手是幽灵”是两回事。