如果你去过大型快递分拣中心,一定见过这样的画面:传送带哗哗转动,包裹一个接一个滑过来,分拣员扫一眼面单,抬手就把包裹甩进不同的分拣口;旁边的贴标机咔哒一声,给每个经过的包裹盖上“已安检”的戳;角落里还有几个戴着白手套的质检员,遇到可疑的纸箱,会小心翼翼地拆开一角,看看里面到底装的是什么。 TypeScript 的类型系统,就是一座这样的“分拣中心”。你写的每一个类型,都是传送带上的包裹;条件类型是分拣员的判断动作,映射类型是贴标机的批量加工,infer 是质检员的拆箱检查。所谓“类型体操”,就是用这些基础动作组合出一套套精密的自动化流水线,在代码编译期就把类型问题拦截下来。 这篇文章,我们就用这座分拣中心当导游图,从最简单的分拣判断讲起,一路走到 infer 拆包、批量贴标、预制工位、递归流水线,最后亲手搭建一条属于你自己的类型安全流水线。走,进分拣中心看看。

岗位关联

学会条件类型与映射类型,你就能为 SDK 和业务代码编写出极致类型约束,把大量运行时错误提前到编译期拦截——这是进阶 TypeScript 开发者的核心分水岭,也是 AI 工具链、前端平台工程里“类型安全第一”的基本功。

目录
  1. 可视化演示
  2. 条件类型基础:分拣员的判断规则
  3. infer 类型推导:拆包质检员的火眼金睛
  4. 映射类型基础:批量贴标的传送带
  5. 工具类型实现:预置的分拣工位
  6. 组合与递归:流水线的无限嵌套
  7. 实战与模式:打造你的智能分拣中心
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

动态演示TS类型体操的核心机制,通过源头/管道/节点/终端四种模式展示关键概念与交互流程。

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

TS类型体操:条件类型 条件类型与分布式条件类型机制 映射类型与键修饰符操作 核心机制可视化 TS类型体操整体架构与关键组件

条件类型基础:分拣员的判断规则

分拣员小李站在传送带前,每天只做一件事:看包裹是否符合“大件标准”。符合,就推进大件通道;不符合,就推进小件通道。他不需要拆包裹,只需要依据面单上的规格做一次“是否符合”的判断。条件类型干的就是这件事——它是类型世界里的分拣员,用一个“是否符合”的问题,把类型引向两条不同的路。

在 TypeScript 中,这个判断写作 T extends U ? X : Y。注意这里的 extends 并不是“继承”,而是可分配性检查:T 这个类型能不能被赋值给 U?换句话说,T 的形状是否满足 U 的要求。这就好比问一句“这个包裹符合大件标准吗?”——符合走 X(大件道),不符合走 Y(小件道)。

条件类型有一个容易踩坑的细节:裸类型参数分发。当 T 是泛型参数、且没有用数组或元组包裹时,如果传入的是联合类型 A | B,TypeScript 不会把 A | B 当成一个整体去判断,而是分别对 A 判断一次、对 B 判断一次,再把结果重新联合起来。这正是后面 Exclude、Extract 等工具类型能工作的地基。

小李分拣久了,总结出一个规律:一次分拣不够,就把两条通道再各接一台分拣机,一层一层往下筛。条件类型嵌套做多了,就自然引出下一节——有些时候,小李还需要拆开包裹看看里面装的是什么。

T extends U  ?  X:Y\displaystyle T \text{ extends } U \;?\; X : Y
可以读作:如果 T 能赋值给 U,就取 X,否则取 Y。整个表达式是一个“类型级的三目运算符”。
  1. T——待分拣的包裹T 是传入条件类型的泛型参数,代表一个待判断的类型,就像传送带上的一个具体包裹。
    TT
  2. extends U——分拣标准检查 T 是否可分配给 U。这不是“包含”,而是“是否满足 U 的形状约束”。
    T extends UT \text{ extends } U
  3. ? XY——分岔路口:判断成立就取 X 分支,不成立取 Y 分支,和 JavaScript 的三目运算符行为一致。
    X:YX : Y

infer 类型推导:拆包质检员的火眼金睛

有的包裹面单模糊,小李看不出该走哪条道。这时质检员老王出马了。老王有个本事:不依赖面单,直接根据包裹外壳的形状和重量,推断里面装的是什么。看到一个鼓鼓囊囊的纸箱,他就能断定“这是一床被子”;听到里面叮当响,就知道“这是一套玻璃杯”。TypeScript 里的 infer 就是老王的这双眼睛——它让我们在不提前声明的情况下,“看穿”一个复杂类型内部的结构。

infer 只能出现在条件类型 extends 子句的右侧,用来声明一个“待推断”的占位类型变量。最常见的写法是:T extends Promise<infer U> ? U : never。这句话的潜台词是:“如果 T 是一个 Promise,那我倒要看看它里面装的是谁——推断出来的那个类型就叫 U,把它取出来;如果 T 根本不是 Promise,那就返回 never(一个'不可能存在'的标记)。”

老王的眼力能用在各种地方:从函数类型里提取参数元组、提取返回值类型,甚至递归地拆开层层嵌套的 Promise。TypeScript 内置的 ReturnType、Parameters 工具类型,本质就是几个“戴着白手套的质检员”。

老王拆包能力虽强,但一次只能拆一个。如果传送带上有一百个包裹等着贴标,靠人力一个个拆就太慢了——这时就要请出下一节的主角:贴标机。

ReturnType⟨T⟩=T extends (...args:any[])⇒infer R?R:never\displaystyle \text{ReturnType}\langle T \rangle = T \text{ extends } (...args: \text{any}[]) \Rightarrow \text{infer } R ? R : \text{never}
这是从函数类型中提取返回值类型的标准写法:只要能推断出函数返回什么,就把返回类型交给 R。
  1. ReturnType\<T\>——提取工位这个工位接收一个函数类型 T,输出它的返回值类型。
    ReturnType⟨T⟩\text{ReturnType}\langle T \rangle
  2. T extends (……argsany[]) => infer R——拆包动作:把函数类型拆开,在返回值位置放一个“待推断”占位符 R。infer 后面的名字是我们自己取的,用来接收推断结果。
    T extends (...args:any[])⇒infer RT \text{ extends } (...args: \text{any}[]) \Rightarrow \text{infer } R
  3. ? Rnever——交付结果:推断成功就把 R 交出来,失败则返回 never 表示“拆不出来”。
    ?R:never? R : \text{never}

映射类型基础:批量贴标的传送带

每个进入分拣中心的包裹,出厂前都要贴上“已安检”标签。让老王一个个贴不现实,分拣中心专门装了一台贴标机:传送带把包裹送进来,贴标机不管包裹内部是什么,只做一件机械的事——在固定位置盖一个戳。映射类型就是这台贴标机,它不关心“值”长什么样,只关心“键”有哪些,然后批量地对每个键做统一加工。

映射类型的语法像一个提前写好的循环:{ [K in keyof T]: T[K] }。我们可以把它读作:“对于 T 的每一个键 K,保留 K 作为新类型的键,并把 T[K] 作为值类型。” 这就是所谓的同态映射——新类型和原类型的“骨架”完全一致,只是每个属性被统一加工过。

映射类型还能玩出更多花样:用 + 或 - 给每个属性添加或移除 readonly、可选修饰符;用 as 子句把键名重新改写(比如把 name 改成 readonlyName),甚至通过条件判断把某些键过滤掉。内置的 Partial、Required、Readonly 工具类型,本质上都是“预设好参数的贴标机”。

贴标机虽然快,但不同的包裹需要不同的标签:有的要贴“易碎品”,有的要贴“加急件”。分拣中心干脆把这些常用的贴标流程预制成若干台专用工位——这就是工具类型的由来。

{[K in keyof T]:T[K]}\displaystyle \{ [K \text{ in } \text{keyof } T] : T[K] \}
这是最基础的映射类型,含义是“遍历 T 的所有键,逐个生成同样键名、同样值类型的新对象类型”。
  1. keyof T——扫描全部键名取出 T 的所有键组成一个联合类型,相当于把包裹面单上的所有信息栏位扫一遍。
    keyof T\text{keyof } T
  2. [K in keyof T]——遍历每个键声明一个循环变量 K,依次等于每一个键名。每循环一次,就生成新类型的一个属性。
    [K in keyof T][K \text{ in } \text{keyof } T]
  3. T[K]——索引取值类型通过索引访问语法取出原类型 T 中键 K 对应的值类型,填入新属性的值位置。
    T[K]T[K]

工具类型实现:预置的分拣工位

分拣中心里最受欢迎的不是通用贴标机,而是几台“专用工位”:Pick 工位只挑出你指定的几个包裹属性,Omit 工位把不要的属性剔除干净,Exclude 工位直接过滤掉联合类型里的违禁成员,Record 工位则像一台万能造箱机——你给它一组键名和一种货物,它立刻造出一个装满同种货物的大箱子。这些工位看似功能各异,但内部无一例外,都是前面条件类型和映射类型这两台基础机器的组合。

先看两个依赖条件分发的工位。Exclude<T, U> 的源码是 T extends U ? never : T:当 T 是裸类型参数时,联合类型的每个成员会被单独送进判断,凡是“可分配”给 U 的成员一律扔进 never 废纸篓,剩下的成员再联合起来。Extract<T, U> 恰好相反,只保留能分配给 U 的成员。再看几个依赖映射的工位:Partial<T> 给每个属性加 ?,Required<T> 减去 ?,Pick<T, K> 用 [P in K] 只遍历指定的键子集,Record<K, T> 则用一个全新的键集合 K 构造对象类型。

理解了这些源码,工具类型就不再是黑魔法,而是你自己也能随手拼出来的预制工位。面试时手写工具类型,拼的就是对这三台“基础机器”的熟练度。

预制工位虽好,但现实中的包裹总是“箱中有箱”:一个纸箱里套着几个小盒子,小盒子里又有更小的袋子。预制工位只会处理一层,遇到嵌套就傻眼——于是分拣中心装上了一条会自我复制的递归流水线。

Exclude⟨T,U⟩=T extends U?never:T\displaystyle \text{Exclude}\langle T, U \rangle = T \text{ extends } U ? \text{never} : T
Exclude 利用条件类型的分发特性,把 T 中每一个能赋值给 U 的成员都替换成 never(剔除),剩下的成员保留,最终得到过滤后的联合类型。
  1. Exclude\<T, U\>——过滤工位从 T 中剔除所有能赋值给 U 的成员。T 一般是联合类型。
    Exclude⟨T,U⟩\text{Exclude}\langle T, U \rangle
  2. T extends U ? neverT——逐个筛检:因为 T 是裸类型参数,联合类型会自动分发,每个成员单独判断;能赋给 U 就进入 never,否则保留原成员。
    T extends U?never:TT \text{ extends } U ? \text{never} : T
  3. 分发后的自动联合所有保留下来的成员自动重新联合,形成最终结果。
    Result=保留的成员1∣保留的成员2∣…\text{Result} = \text{保留的成员}_{1} \mid \text{保留的成员}_{2} \mid \ldots

组合与递归:流水线的无限嵌套

分拣中心收到一批“套娃包裹”——大箱子套小箱子,小箱子套更小的盒子。质检员老王决定把流水线改造一下:每当拆出一个新箱子,就把这个箱子重新送回流水线开头,继续执行同一套分拣、贴标流程,直到拆出来的东西不再是个箱子为止。这就是类型体操里的递归类型:类型定义在自身内部引用自己,同时必须有一个明确的“终止条件”,防止无限循环下去。

递归类型有两个关键规则要牢记。第一是联合分发:裸类型参数在条件类型中会自动逐个分发,这既是工具类型的利器,也是递归时容易出 bug 的地方。第二是终止条件:递归的类型本体必须用一个条件类型判断“当前值是否还要继续拆”,比如 T extends object ? 递归处理 : 保持原样,这样基础类型(string、number 等没有 keyof 成员的类型)就会直接停下,不会无限套娃。

经典的 DeepReadonly 就是这样一个递归贴标机:它遍历对象的所有属性,如果某个属性的值仍然是一个对象,就递归地对它再贴一层 readonly 标签;如果遇到基础类型,就直接返回原样。另一个经典案例 Awaited 专治嵌套 Promise,一层层地拆,直到拆出真正的值类型。

递归流水线让分拣中心具备了处理任意嵌套结构的能力。至此,分拣、拆包、贴标、预制工位、递归迭代——所有基础模块都齐了。是时候把它们串成一条完整的生产线,去解决真实世界的类型难题了。

DeepReadonly⟨T⟩={[K in keyof T]:T[K] extends object ?DeepReadonly⟨T[K]⟩:T[K]}\displaystyle \text{DeepReadonly}\langle T \rangle = \{ [K \text{ in } \text{keyof } T] : T[K] \text{ extends object } ? \text{DeepReadonly}\langle T[K] \rangle : T[K] \}
DeepReadonly 的含义是:遍历 T 的每个键,如果值类型仍然是对象,就递归地给它的所有属性加上 readonly;如果值类型是基础类型,就直接保留原样。
  1. 遍历外层键用 [K in keyof T] 遍历 T 的所有属性,逐个检查值类型。
    [K in keyof T][K \text{ in } \text{keyof } T]
  2. 递归判断核心取出值类型 T[K],用条件类型判断它是否还是对象;是,则对 T[K] 再调用一次 DeepReadonly。
    T[K] extends object ?DeepReadonly⟨T[K]⟩:T[K]T[K] \text{ extends object } ? \text{DeepReadonly}\langle T[K] \rangle : T[K]
  3. 终止条件当值类型是 string、number 等基础类型时,extends object 不成立,直接返回 T[K],递归在此终止。
    :T[K]: T[K]

实战与模式:打造你的智能分拣中心

现在的分拣中心已经全副武装:条件类型负责分流,infer 负责拆包,映射类型负责贴标,工具类型是现成工位,递归则打通了任意深度的嵌套。把这些模块组合起来,就能为真实业务搭建类型安全的生产线。这里给出三个最常见的实战模式。

第一个模式是 API 响应类型推断。后端接口往往返回 Promise<ApiResponse<User>> 这样的嵌套结构,前端如果手动维护解包后的类型,接口一变就全线漂移。用 Awaited 工具类型,一行就能把嵌套的 Promise 自动拆到最里层,保证前端拿到的类型和后端返回的结构永远同步。第二个模式是事件映射表。一个复杂的 AI Agent 系统有几十种事件,每种事件携带不同的 payload。用映射类型把事件名和 payload 一一对应起来,订阅事件时 TypeScript 就能精准推断出处理函数的参数类型,写错属性名直接编译报错。第三个模式是状态机类型。用条件类型约束状态转换的合法性——比如“已取消”状态不允许直接跳到“已发货”,非法转换在编译期就被拦截。

到最后,类型体操的核心心法不过一句话:把每一个类型都想象成传送带上的包裹,用分拣、拆包、贴标这些基础动作去加工它。你掌握的模块越多,能搭出的生产线就越复杂、越精密。

回看整座分拣中心:条件类型是那双分拣的手,infer 是那双透视的眼,映射类型是那台不知疲倦的贴标机,工具类型是被反复调用的预制工位,递归让流水线拥有了无限深度。下一次当你面对一个复杂类型时,不妨问问自己:这个包裹,应该走哪条道?

Awaited⟨T⟩=T extends Promise⟨infer U⟩?Awaited⟨U⟩:T\displaystyle \text{Awaited}\langle T \rangle = T \text{ extends Promise}\langle \text{infer } U \rangle ? \text{Awaited}\langle U \rangle : T
Awaited 是递归 infer 的经典应用:如果 T 是 Promise,就拆出内部类型 U,再对 U 继续调用 Awaited,直到拆出非 Promise 的真实类型。
  1. 判断是否 PromiseT extends Promise<infer U> 同时完成两件事——判断 T 是不是 Promise,并把内部的类型推断为 U。
    T extends Promise⟨infer U⟩T \text{ extends Promise}\langle \text{infer } U \rangle
  2. 递归拆解取 U 后再次调用 Awaited<U>,处理 Promise<Promise<...>> 这类多层嵌套。
    Awaited⟨U⟩\text{Awaited}\langle U \rangle
  3. 终止条件当 T 不再是 Promise 时,直接返回 T 本身,递归结束。
    :T: T

AI 应用场景

场景一:类型安全的 AI 模型调用链

阶段:AI 应用开发中的 SDK 设计

解决的问题:在对接 GPT、Claude、Gemini 等多个大模型时,各家请求参数结构不同,手工维护请求类型极易在模型升级后“类型漂移”。利用条件类型把模型名联合类型分发到各自的请求类型,再结合工具类型自动推导响应类型,调用链全程类型安全。

具体案例:设计一个多模型网关,type Model = 'gpt-4o' | 'claude-3' | 'gemini-pro',用条件类型将每个模型名映射到对应的配置类型,写错模型名或传错参数在编译期直接报错,AI 工程团队再也不用在运行时才发现参数不匹配。

场景二:Agent 事件系统的精准类型推导

阶段:Agent 平台的类型基础设施构建

解决的问题:LLM Agent 会发出 tool_call、message、error 等几十种事件,每种事件 payload 结构不同。传统做法用 any 接收事件,处理函数里全靠运行时判断,极易出错。用映射类型把事件名映射到 payload 类型,订阅时自动获得精准参数提示。

具体案例:基于事件注册表定义 interface AgentEventMap { tool_call: ToolCallPayload; message: MessagePayload; error: ErrorPayload },配合 keyof 和索引访问类型实现 on<K extends keyof AgentEventMap>(event: K, handler: (payload: AgentEventMap[K]) => void),订阅事件时编译器自动给出正确的 payload 类型。

场景三:数据管道 schema 类型守卫

阶段:数据处理管道的类型校验

解决的问题:数据管道从多数据源拉取数据,schema 与代码类型经常不一致,导致下游模型训练时字段缺失。用条件类型和 infer 从数据源的 schema 声明中自动推导出管道节点的输入输出类型,编译期即可发现字段不匹配。

具体案例:ETL 管道中,上游表 users 经 select<"id" | "name"> 后自动生成只含这两个字段的输出类型,再经 where 条件过滤后类型仍然保持精确,整个管道像一条类型安全的流水线。

Python 演示

python48 行
class TypeSim:    """简化版TS类型体操模拟器:用集合模拟联合类型的分发与映射。"""     def __init__(self):        self.env = {}     def conditional(self, t, u, true_fn, false_fn):        """模拟 T extends U ? X : Y 的分发逻辑。"""        results = []        for member in t:            if member in u:                results.append(true_fn(member))            else:                results.append(false_fn(member))        return set(results)     def mapped(self, t, fn):        """模拟映射类型 { [K in keyof T]: F }。"""        return {k: fn(v) for k, v in t.items()}     def infer_from_ret(self, fn):        """用函数的返回注解模拟 infer 提取返回值类型。"""        sig = fn.__annotations__        return sig.get("return", type(None))  def demo():    sim = TypeSim()     union = {str, int, float, bool}    result = sim.conditional(union, {str, bool},                             lambda x: x.__name__ + "_OK",                             lambda x: x.__name__ + "_NO")    print("条件分发结果:", sorted(result))     obj = {"name": str, "age": int}    readonly_obj = sim.mapped(obj, lambda v: (v, True))    print("映射结果:", readonly_obj)     def f() -> float:        ...     inferred = sim.infer_from_ret(f)    print("infer 提取:", inferred)  if __name__ == "__main__":    demo()
目的:用 Python 模拟 TypeScript 类型体操的三个核心机制——条件类型分发(分拣)、映射类型(贴标)、infer 提取(拆包),帮助你在不依赖 TS 编译器的情况下直观理解类型运算的本质。
  1. class TypeSim:定义模拟器,等价于一座迷你类型分拣中心。
  2. def conditional(self, t, u, true_fn, false_fn):对应条件类型 T extends U ? X : Y 的分发逻辑。
  3. results.append(true_fn(member) if member in u else false_fn(member))对联合类型的每个成员单独判断,模拟裸类型参数的分发行为。
  4. return set(results)把结果重新联合成一个集合,对应 TS 中分发后的联合类型。
  5. def mapped(self, t, fn):对应映射类型 { [K in keyof T]: F },遍历键并逐个加工。
  6. return {k: fn(v) for k, v in t.items()}字典推导式模拟映射类型的“批量贴标”。
  7. sig = fn.__annotations__通过函数的注解字典模拟 infer 从函数签名提取信息。
  8. union = {str, int, float, bool}用 Python 的集合模拟 TS 的联合类型。
  9. readonly_obj = sim.mapped(obj, lambda v: (v, True))给每个属性打上“只读”标记,模拟 Readonly 工具类型。
  10. inferred = sim.infer_from_ret(f)从函数的返回类型注解中推断类型,等价于 ReturnType。

常见误区

误区“TS 的条件类型跟 JavaScript 的三目运算符一样,代码运行到那一行才会判断走哪个分支,所以可以用来做运行时逻辑。”

正解条件类型完全在编译期求值,最终产物 JavaScript 中根本不存在任何类型代码。它像分拣员在包裹入场前就完成分流,而不是等包裹送到客户手里才判断。类型体操的价值恰恰在于“提前判断、提前拦截”,把错误挡在编译期。

为何常见:因为语法 T extends U ? X : Y 长得太像运行时三目运算符,初学者很容易把类型空间和值空间混为一谈。

误区“`T extends U ? X : Y` 里如果 T 是 `string | number`,那就整体判断一次,要么返回 X 要么返回 Y。”

正解当 T 是裸类型参数时,TypeScript 会把 string | number 拆成 string 和 number 分别判断,再把两个结果联合起来。这不是做不做得到的问题,而是 TS 的既定规则——正是这条“分发”规则撑起了 Exclude、Extract 等工具类型。若不想分发,可以用 [T] 把 T 包起来。

为何常见:日常编程中“一个整体判断一次”的直觉太强,很少有人意识到编译器的泛型参数分发机制。

误区“infer 就是一个声明类型变量的关键字,我想在哪写就在哪写,比如 `type X = infer U`。”

正解infer 只能出现在条件类型 extends 子句的右侧,且必须配合一个“待推断”的结构出现,比如 Promise<infer U>、(...args: infer P) => any。它就像一个只能出现在拆包台上的质检员——你给了他一个包裹,他才能告诉你里面是什么。裸写 infer 没有任何上下文,编译器无从推断。

为何常见:infer 的语法关键字过于“自由”,让人误以为它是通用的类型声明工具,而忽略了它“从已有结构中提取”的本质。

误区“`Partial<T>` 把 T 的属性改成可选后,原来的 T 也变成可选了,所以要注意别污染原始类型。”

正解TypeScript 类型是不可变的。映射类型总是产出一个全新类型,原始类型 T 永远不会被改动。就像贴标机给包裹贴了“易碎品”标签,并不会改变包裹内部的货物——只是新产生了一个带标签的副本。这个误区会导致开发者不敢放心使用工具类型,白白错过类型体操的便利。

为何常见:JavaScript 中数组的 sort 等方法是原地修改,这种“可变质变”的思维惯性被错误地带到了类型世界。

面试题

Q1. 条件类型 T extends U ? X : Y 中,当 T 是联合类型 A | B 时会如何判断?

单选题 · easy

  • A.整体判断后返回单个结果
  • B.分发后每个成员单独判断再联合← 正确答案
  • C.编译失败且没有任何结果
  • D.强制跳过判断直接返回 Y

答案:B。当 T 是裸类型参数时,条件类型对联合类型自动分发:A | B 会分别判断 A 和 B,再把结果联合起来。就像分拣员把联合包裹拆成两个单件分别分拣,而不是把整个包裹班次一刀切。A 是常见误解,C 无依据,D 违背条件语义。

Q2. type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T 中 infer U 的作用是什么?

单选题 · easy

  • A.固定 U 为 string 类型
  • B.推断 Promise 的真实类型← 正确答案
  • C.使 U 强制等于泛型 T
  • D.直接把 U 当作 never

答案:B。infer U 声明一个待推断的占位类型,从 Promise<U> 的结构中提取出 U。这就像质检员拆开快递盒,看穿里面有被子还是玻璃杯——盒子是谁不重要,重要的是里面是什么。A/C/D 都是对 infer 的误解。

Q3. 映射类型 { [K in keyof T]: T[K] } 输出结果最接近哪个工具类型?

单选题 · easy

  • A.等价于工具类型 Required
  • B.保持键名与值类型不变的同态映射← 正确答案
  • C.把所有值类型转为字符串类型
  • D.等价于 Pick 只留下部分键

答案:B。[K in keyof T] 遍历 T 的所有键,T[K] 原样保留值类型,得到的是一个骨架完全一致的新类型,这是“同态映射”。Required 会修改修饰符,C 会改变值类型,Pick 只保留部分键,均不符合。

Q4. 下列哪个类型正确实现了 Exclude&lt;T, U&gt;?

单选题 · mid

  • A.T extends U ? T : never
  • B.T extends U ? never : T← 正确答案
  • C.U extends T ? never : T
  • D.T extends unknown ? never : T

答案:B。Exclude 要从 T 中剔除能赋给 U 的成员,所以“能赋给 U”的分支必须返回 never(剔除),其他分支保留 T。A 恰好相反,实现的是 Extract;C 判断方向反了;D 把所有 T 都变成 never。

Q5. 提取函数参数元组类型的正确写法是?

单选题 · mid

  • A.T extends (...args: infer P) => any ? P : never← 正确答案
  • B.T extends (...args: infer N) => any ? N : never
  • C.T extends (...args: any) => infer P ? P : never
  • D.T extends (...args: any) => any ? infer never : never

答案:A。要提取参数类型,infer 必须放在参数列表位置 (...args: infer P)。B 虽然语法正确但 N 只是变量名不同,效果与 A 相同,题目考查的是位置——C 把 infer 放到返回值位置提取的是返回类型,D 在冒号后使用 infer 非法。分拣时要拆哪个部位,眼睛就要盯在哪个部位。

Q6. 条件类型中“裸类型参数”指的是什么?

单选题 · mid

  • A.用数组包裹后仍会正常分发
  • B.直接被使用的泛型参数本身← 正确答案
  • C.仅包含基础类型的泛型参数
  • D.声明了 extends 约束的泛型参数

答案:B。只有当泛型参数“裸”出现——没有被 [T]、T[] 等结构包裹——联合类型才会自动分发。C 描述的是值域而非形态,D 描述的是约束,A 说反了:被数组包裹后不会分发。

Q7. 为什么 DeepReadonly 必须用 T extends object 作为递归终止条件?

单选题 · hard

  • A.因为 object 无法被递归遍历
  • B.因为基础类型触发递归会无限循环← 正确答案
  • C.因为 never 会中断一切递归过程
  • D.因为 readonly 只对 object 生效

答案:B。string、number 等基础类型没有 keyof 成员,若不对它们终止,泛型 T 会被无限次传入 DeepReadonly,导致类型实例化过深报错。分拣中心遇到空盒子必须停下,而不是把空盒子再送回流水线。A、C、D 都不是递归终止的核心原因。

Q8. 以下哪些工具类型依赖“条件类型 + infer”实现?

多选题 · mid

  • A.ReturnType<T>← 正确答案
  • B.Parameters<T>← 正确答案
  • C.Record<K, V>
  • D.Awaited<T>← 正确答案

答案:ABD。ReturnType 用 infer R 提取返回值,Parameters 用 infer P 提取参数元组,Awaited 用 infer U 递归拆 Promise,三个都依赖 infer。Record 是纯映射类型,不涉及任何推断。

Q9. 关于映射类型的键重映射 [K in keyof T as NewKey],正确的说法有哪些?

多选题 · hard

  • A.重映射键可以用模板字面量生成← 正确答案
  • B.重映射可通过条件判断过滤目标键← 正确答案
  • C.重映射之后会改变原对象的键
  • D.重映射只能把键改为 union 联合类型

答案:AB。as 子句支持模板字面量语法,如 as get${K & string}`,也支持通过 K extends …… ? never : K` 过滤键。C 错误:重映射产生新类型,原类型不变;D 错误:键可以改成字符串字面量、symbol 等,不止 union 类型。

Q10. 以下哪些工具类型用于从联合类型中筛选成员?

多选题 · easy

  • A.Exclude 用于剔除联合类型成员← 正确答案
  • B.Extract 用于提取指定联合成员← 正确答案
  • C.Partial 用于把全部键变为可选
  • D.Required 用于把可选键变为必选

答案:AB。Exclude 和 Extract 是“筛选类”工具类型,Exclude 剔除指定成员,Extract 提取指定成员,都基于条件类型的可分配性分发。Partial 和 Required 是“修饰符类”工具类型,作用于对象属性而非联合类型成员。