想象一座深夜仍在运转的智能快递分拣中心:传送带把形状各异的包裹送过来,一台装着蓝色扫描灯的分拣机立在中央。包裹经过时,扫描灯“叮”地一照——能塞进标准箱 U 的,被推上 X 通道;塞不进去的,滑向 Y 通道。同一批货里混着装在一起的单件,机器会拆开逐个检查;有的包裹里还藏着一本说明书,机器有专门的窗口能把它抽出来看一眼;货架区的工人则按月批量给所有货架贴锁、撕标签、换铭牌。这套分拣中心的一切操作,恰好就是 TypeScript 条件类型与映射类型的日常:T extends U ? X : Y 的问答、联合类型的逐一判型、infer 的拆包取值、[K in keyof T] 的批量改造。本文就用这座分拣中心作向导,带你从零看懂类型体操的每一台机器。
对 AI 从业者而言,类型体操是给大模型应用套上“编译期保险丝”的必修课:让 Agent 工具调用的入参、LLM 输出的 Schema、AI 前端的状态流转在运行之前就被类型系统勘误,大幅减少线上事故。
目录
可视化演示
展示TypeScript条件类型的分支推断与映射类型的属性变换流程,揭示类型系统如何实现条件判断与批量属性转换。
本节配有交互动效(进入视口后自动加载,约 3KB)。
条件类型与映射类型 条件类型:T extends U ? X : Y 分支推断 映射类型:{[K in keyof T]: V} 属性变换 分布式条件类型:联合类型逐成员推断 Conditional = T extends U ? TrueType : FalseType TypeScript高级类型系统的条件判断与映射变换
条件类型基础:分拣机的第一问
分拣中心的传送带把包裹送到蓝色扫描灯下,机器只问一句话:“这个包裹能塞进标准箱 U 吗?”能,推入 X 通道;不能,滑向 Y 通道。TypeScript 的条件类型就是这台机器——它把“能不能塞进”翻译成“T 是否结构兼容于 U”,一句话问完,两个通道二选一。
条件类型的完整写法是 T extends U ? X : Y。注意这里的 extends 不是类的继承,而是一台“结构兼容扫描仪”:只要 T 拥有 U 要求的全部形状,哪怕 T 还多出几个属性,也算通过。就好比分拣机的标准箱只关心长宽高,包裹里多塞一块泡沫都不影响它能被装进去。这种“问规格”发生在编译期,运行时根本不存在这台扫描灯。
条件类型可以出现在任何类型能出现的地方,比如 type IsString<T> = T extends string ? true : false; 用起来就像给类型世界写 if/else。它常与泛型配合,让类型根据输入的不同自动选择出口,这正是类型体操里最基础的“起手式”。
回到分拣中心:扫描灯“叮”的一声之后,包裹已经消失在某一侧的传送带上。条件类型的妙处也在于此——它不问过程,只问结果,一锤定音地根据输入决定输出,类型自此有了“判断力”。
- T待检包裹:T 是我们传入的原始类型,可能是泛型参数、联合类型或某个对象属性的类型。
- extends U扫描规格:把 T 与 U 做结构兼容性检查,能塞进 U 的规格箱就算通过扫描。
- ? XY:双通道分流:扫描通过走 X 通道得到 X,不通过则滑向 Y 通道得到 Y。
分布式条件:一箱混装的独立判型
分拣中心偶尔会接到一箱混装的包裹:箱子里同时叠着字符串包裹、数字包裹和布尔包裹。蓝灯扫描仪的规矩是:不判断整箱,而是把箱子拆开,让每个单件独立过一遍扫描灯,最后再把各自的结果重新拼成一箱。这个规矩叫“分布式条件类型”。
对应的语法事实是:当 T 是一个裸的泛型参数,且调用时传入的是联合类型 string | number | boolean 时,T extends U ? X : Y 会自动对每个成员分别求值。比如 T extends string ? 1 : 2,传入 string | number 时,结果是 1 | 2,而不是整个联合被一票否决议成 2。
内置工具类型 Exclude 就是典型的分布式机器:Exclude<T, U> 把 T 中所有能赋给 U 的成员换成 never(相当于在传送带上直接扔掉),剩下的成员自动合并返回。若想关掉“拆箱”,把 T 用方括号包起来写成 [T] extends [U],就像给整箱包裹加一道封条,扫描仪就只能整箱判断了。
这就像混装箱被拆开、逐件扫描、再重新装箱:每个单件都保有独立判断的权利,谁也不会因为“同箱的兄弟不合格”而被连坐。分布式条件类型让联合类型的处理精确到“每一个成员”,是类型体操里最常用的机关之一。
Exclude 是“剔除机器”:从 T 里删掉所有 U 能接受的成员,never 会被自动丢弃,残余成员重组成新的联合类型。- Exclude(T,U)剔除机器:内置的条件类型工具,作用是从 T 中筛掉能赋给 U 的成员。
- T 自动拆包T 是裸类型参数时,联合类型的每个成员会被拆成单件逐一处理。
- 逐成员判定每个成员独立走一遍 extends 扫描,命中的被替换成 never。
- 结果合并never 被系统自动过滤,存活的成员重新组成联合类型。
infer 关键字:拆包裹取说明书
有些包裹里夹着一本说明书,写着“本包裹实际装载的是 X 类型”。分拣中心在扫描灯旁边开了一个检查窗口:窗口上写着“infer R“,意思是”让我看看里面到底装了什么,并把答案记到 R 上”。这个窗口就是 infer 关键字。
infer 只能出现在条件类型的 extends 匹配模式里。最经典的应用是提取函数返回类型:ReturnType<F> 会先检查 F 是否匹配“一个函数”的形态,匹配时在返回位置放一个 infer R 占位符,让 TypeScript 把真实的返回类型填进 R。就像一个窗口把函数签名“摊开”给你看:参数那一段用 ...args: any[] 模糊处理,返回值那一段由 infer 精确抄录。
换个例子:type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never; 可以从元组里提取第一个元素的类型。infer 的位置决定它能提取什么——放在返回类型处取返回值,放在元组头部取首元素,放在 Promise 内部就取被包裹的内层类型。它的作用域仅限于当前条件分支,出了这个分支,R 就作废了。
检查窗口的存在,让“拆包裹”变得合法且有边界:infer 不是魔法,它必须依附于一个具体的模式匹配。就像分拣员只能在指定窗口拆包,换一个位置(比如直接写 type X = infer R)就不被允许。学会在正确的位置安放 infer,类型体操就入门了一半。
- F函数类型本体:F 必须能匹配函数签名形态,比如
(x: number) => string。 - 参数占位
(...args: any[])张开一张大网,把函数签名中的参数部分整体承接。 - infer R 提取返回值在返回位置放一个占位符,让 TS 把真实的返回类型填入 R。
- 兜底 any若 F 不是函数,匹配失败,走假分支得到 any。
映射类型:批量改造货架车间
分拣中心的深处是货架车间。主管发话:“这排货架,每个抽屉都贴一张'可选'标签;那排货架,每个抽屉都加一把锁。”工人不需要逐个抽屉手工处理,而是拿着钥匙串 keyof T,用 in 循环从第一把钥匙走到最后一把,全程机械化操作。这套批量改造流程叫映射类型。
映射类型的写法是 { [K in keyof T]: T[K] }。keyof T 把对象的所有键收集成联合类型,好比把整排货架的钥匙串到一起;K in 像 for 循环一样逐个取出键名;T[K] 则用键去索引原来的类型,取出对应抽屉里的值类型。三者配合,就能“照原样复制”一个对象类型——或者在此基础上做任意改造。
说它是“同态变换”是因为输出类型与输入类型共享同一套键:键没有增减,只是每个键对应的值类型被统一处理。比如 { [K in keyof T]: T[K][] } 会让每个属性都变成数组。货架还是那排货架,抽屉里的东西全被换成了“数组包装”。映射类型管的是“形状”,不改“骨架”。
货架车间的哲学是“批量化、机械化”:只要给出钥匙串和处理规则,整排货架一次改造完成。映射类型因此成为所有内置工具类型的底盘——下一节的修饰符增减,就是在改造指令上多写了几个符号而已。
- keyof T取钥匙串:把 T 的所有键收集成联合类型,比如
keyof { a: number; b: string }得到"a" | "b"。 - K in遍历钥匙:
in像循环逐个取出键名,K 依次取"a"、再取"b"。 - T[K]打开对应抽屉:用键 K 索引原类型 T,取该键对应的值类型。
修饰符增减:贴锁、撕签与重贴铭牌
车间主管的指令不只是“改造值类型”,还能动属性本身的“属性”:加一把锁,叫 readonly;贴一张“可选”标签,叫 ?;反过来,撕掉标签就用减号。TypeScript 给映射类型准备了专门的修饰符语法:+ 表示添加,- 表示移除。
最常见的操作有两个。一是加锁:{ readonly [K in keyof T]: T[K] } 让每个属性都变成只读,编译期禁止再赋值;二是加减可选标记:{ [K in keyof T]-?: T[K] } 把所有可选属性变成必选,这就是内置工具 Required<T> 的实现原理。+? 则反过来把每个属性都变成可选,也就是 Partial<T> 的实现。
更高级的玩法是 as 键重映射:{ [K in keyof T as \get${Capitalize<K>}\]: T[K] } 可以把键名改造成 getXxx 的形式。好比给货架换一套新铭牌,键还是那些键,但名字全变了。修饰符加减配合 as 重映射,让映射类型从“复制”进化成“再创作”。
除了解除可选,还有一套对应的“重贴铭牌”公式:
修饰符就像是货架上的标签系统:readonly 是“请勿擅动”的警示牌,? 是“可缺省”的便利贴,减号是撕下旧标签的手。掌握这些符号的增删,就能像车间主管一样精准控制每个属性的“脾气”。
- as 重贴标签
as允许在遍历过程中改写键名,生成一个全新的键集。 - Uppercase 内置模板把每个键名转成大写,像给货架换上新铭牌。
内置工具类型:标准化改造机器
分拣中心备有一套标准化机器,每台机器背后都是一段我们刚刚学过的映射或条件表达式。Partial<T> 全员可选,Required<T> 全员必选,Readonly<T> 全员加锁——三台机器共用同一个遍历底盘,只是修饰符不同。Record<K, V> 则不一样:它不改造已有对象,而是用 K 这串钥匙凭空造出一排新货架,每个抽屉统一放 V 类型的货物。
Pick<T, K> 是“挑拣机”:从 T 中只保留键集 K 里的属性;Omit<T, K> 反过来,剔除 K 里的属性。Exclude<T, U> 和 Extract<T, U> 是条件类型家族的成员,前者剔除、后者保留,都依赖分布式展开。至于 ReturnType 和 Parameters,则是 infer 家族的代表,专攻函数签名的拆解。
把这些机器排在一起会发现:所谓内置工具类型,不过是映射类型与条件类型的“官方预置模板”。理解它们的实现原理,比记忆 API 更重要——因为一旦看穿底盘,你就能自己组装出任何定制机器。
这些标准化机器解决了 80% 的日常类型需求:表单提交用 Partial,配置补丁用 Partial,键值表用 Record,白名单字段用 Pick。车间里备好了通用机器,剩下的 20% 才轮到我们亲手造新机器——那就是实战体操的舞台。
- Partial(T)全员可选:遍历 T 的键,每个键都加上可选标记。
- Record(K,V)批量制造:K 是键的联合,P 逐个取键,V 作为统一的值类型。
- 属性访问保持两种机器都不改动原有属性对应的值类型,只改“键的存在方式”。
实战类型体操:组合流水线
真正的“体操”发生在机器不够用的时候。假设我们需要 DeepPartial<T>:不仅顶层属性可选,嵌套对象里的每一层属性也都可选。普通的 Partial 只改造一层就停工了,好比车间主管只给货架第一排贴了标签,里层的抽屉一概不管。
解法是把映射类型和条件类型组合成递归流水线:遍历每个键,加可选标记;然后问一句“这个值类型是复合包裹吗?是,就把它送进流水线再改造一层;不是(是 string、number 这类原子类型),就直接放行”。递归的出口是原子类型,保证了流水线不会无限循环。
这类组合在真实项目中俯拾皆是:从函数签名里抠出参数类型、从 Promise<T> 里解包出 T、把数据库实体映射成 API 响应 DTO、按权限裁剪用户字段……每一样都是“映射 + 条件 + infer”三种机器的组合拳。体操的名字由此而来:动作不新,组合见功夫。
回到分拣中心看这条终极流水线:传送带每过一个包裹就贴一张“可选”标签,若发现里面还是箱子,就拆开再走一遍同样的流程;拆到底,看到牙刷、文件这些没法再拆的东西,就放它离开。体操的精髓不在某台机器多神奇,而在于你能否把机器按正确的顺序接起来,让类型像流水线一样自动流转。
- 逐键加问号先给每个键加上可选标记,允许外层缺省。
- 判断是否嵌套用
extends object检测该值类型是不是一个复合包裹。 - 是则递归遇到嵌套对象就递归调用自身,把改造贯彻到底层。
- 否则保留原子基础类型原样返回,不再深挖。
AI 应用场景
LLM 对话消息的判别联合类型
阶段:前端应用层 / 类型安全工程
解决的问题:AI 聊天应用中,消息可能是用户消息、助手消息、工具调用消息,每种消息的载荷结构完全不同。传统做法用一个大而全的接口,运行时再靠 switch 猜结构,容易漏判导致白屏。利用条件类型和判别联合,kind 字段驱动类型收窄,payload 类型在编译期就被精确锁定。
具体案例:定义 type ChatMessage = { kind: 'user', content: string } | { kind: 'tool_call', name: string, args: Record<string, unknown> },渲染组件用条件类型根据 kind 精确推导渲染分支,Agent 每新增一种消息类型,编译器立刻标出所有遗漏的分支处理。
Agent 工具调用的参数 Schema 提取
阶段:Agent 编排层 / Function Calling
解决的问题:让大模型调用工具时,入参必须和工具函数签名完全一致。手写一遍入参类型再写一遍函数,两边极易漂移。用 infer 从工具函数签名直接提取入参类型,保证“一份签名,处处复用”,模型生成的 JSON 参数在编译期就接受类型校验。
具体案例:写一个 defineTool<F extends (args: any) => any>(fn: F) 工具,内部用 Parameters<F>[0](基于 infer 实现)取出第一个参数类型作为 inputSchema 的 TS 类型,前端表单根据该类型自动生成输入控件,同时把 schema 喂给 LLM 做工具描述。
训练平台配置补丁的 DTO 映射
阶段:MLOps / 数据管道工程化
解决的问题:AI 训练平台的配置项有几十个字段,PATCH 接口只允许提交部分字段,传统做法复制整份配置再手动删字段。用 Partial<Config> 生成补丁类型,用 Record<MetricName, number> 生成指标表类型,映射类型让 DTO 与内部模型保持同构。
具体案例:训练配置 TrainConfig 有 learningRate、batchSize、epochs 等字段,type Patch = Partial<TrainConfig> 直接作为更新接口的入参类型;平台用映射类型从指标名联合自动生成 Record<string, number> 的监控上报类型,新增指标只需改一处枚举。
Python 演示
"""模拟 TS 条件类型与映射类型的小判型器:- conditional_type 对应 T extends U ? X : Y- distributive_conditional 模拟联合类型逐成员展开- mapped_type 对应映射类型 { [K in keyof T]: F(T[K]) }- PromiseBox 演示 infer 解包内部类型"""from typing import Any, Callable, List, TypeVar T = TypeVar("T")U = TypeVar("U") def conditional_type(t: type, u: type, x: str, y: str) -> str: """若 t 可赋给 u 则走 X 通道, 否则走 Y 通道""" try: return x if issubclass(t, u) else y except TypeError: return y def distributive_conditional(items: List[type], u: type, x: str, y: str) -> List[str]: """把类型列表当成联合类型, 对每个成员独立判型""" return [conditional_type(it, u, x, y) for it in items] def mapped_type(data: dict, transform: Callable[[Any], Any]) -> dict: """逐键批量施加变换, 生成新对象类型""" return {key: transform(val) for key, val in data.items()} class PromiseBox: """模拟 Promise<T>, inner 是被藏起来的内部类型""" def __init__(self, inner: type) -> None: self.inner = inner def infer_inner(box: PromiseBox): """扮演 infer R: 从包装中拆出内部类型""" return box.inner if isinstance(box, PromiseBox) else None def main() -> None: results = distributive_conditional([int, str, float], int, "X通道", "Y通道") print("分布式判定:", results) raw = {"id": "42", "age": 18} locked = mapped_type(raw, lambda v: "[readonly]" + str(v)) print("映射加锁:", locked) box = PromiseBox(str) print("infer 解包:", infer_inner(box)) if __name__ == "__main__": main()
issubclass、列表推导和字典推导,直观复刻 TS 条件类型、分布式展开、映射类型和 infer 解包的判型逻辑。def conditional_type(t: type, u: type, x: str, y: str) -> str:定义判型函数,四个参数正好对应T extends U ? X : Y的四个角色。return x if issubclass(t, u) else y核心判型逻辑:issubclass扮演 extends 扫描仪,通过走 X,不通过走 Y。def distributive_conditional(items: List[type], u: type, x: str, y: str) -> List[str]:接收一个类型列表,模拟“联合类型拆成逐个成员”。return [conditional_type(it, u, x, y) for it in items]列表推导对每个成员独立判型,就是分布式条件类型的逐成员展开。def mapped_type(data: dict, transform: Callable[[Any], Any]) -> dict:映射类型的通用形态:对每个键值批量施加变换。return {key: transform(val) for key, val in data.items()}字典推导模拟{ [K in keyof T]: F(T[K]) }的逐键遍历。def infer_inner(box: PromiseBox):模拟 infer R:把包装里的内部类型重新拆出来。results = distributive_conditional([int, str, float], int, "X通道", "Y通道")一次典型的分布式判型调用:三个类型各走各的通道。
常见误区
误区“我以为 `T extends U` 就像 JS 的 `===`,运行时拿两个值比一比,条件类型是程序跑起来之后才执行的分支。”
正解条件类型的一切都发生在编译期。extends 是“结构可赋值性”检查,像分拣机只看包裹外包装的规格标签,不打开包裹,更不等着包裹寄出才判断。编译完成后,这些类型分支全部消失,运行时里没有任何分拣机,也没有任何扫描动作。
为何常见:因为 JS 里有三元运算符、有 typeof,初学者很容易把类型层面的语法和运行时的语法画等号,忽略了 TypeScript 类型是“编译期虚拟世界”这一事实。
误区“`T extends U ? X : Y` 里 T 如果是 `string | number`,我以为会把整个联合当成一个整体去和 U 比,结果要么整体通过、要么整体拒绝。”
正解裸类型参数会被分布式展开:机器先把联合里的每个成员拆开,单独过一遍 extends,再把各自的结果合并成新联合。就像一箱混装的快递会被拆成单件逐一扫描,而不是整箱一次过检。想整体判断,可以用 [T] extends [U] 把包裹封进箱子,阻止拆件。
为何常见:日常 if 语句总是把一个值整体判断,人脑默认“类型条件也是整体判断”,于是漏掉了裸类型参数触发分布式展开这个特殊规则。
误区“我以为 `infer R` 像正则表达式里的捕获组,想在哪里挖类型就在哪里写占位符,甚至可以在映射类型之外直接声明来用。”
正解infer 只能出现在条件类型的 extends 匹配模式内部,且 R 的作用域只覆盖当前分支。它像分拣中心里唯一的那扇检查窗口——包裹必须从窗口经过,里面的东西才被看见;脱离窗口直接拆包是不被允许的。位置错了,编译器立刻报错。
为何常见:正则捕获组深入人心,而 infer 长得确实像捕获组,导致它的“位置必须依附于匹配模式”这一约束被习惯性忽略。
误区“我以为映射类型里加了 `readonly` 之后,对象在运行时就像 `Object.freeze` 一样完全不能改写,改了就抛异常。”
正解readonly 是编译期的锁,只在类型检查时禁止重新赋值;运行时该对象仍然是普通 JS 对象,照样能改。它像贴在货架上的“请勿擅动”标签,约束的是开发者,不是货架本身。真正想运行时冻结,得用 Object.freeze,这是两套完全不同的机制。
为何常见:Object.freeze 返回的类型也带 readonly 标记,两者名字相似、表现相关,初学者很容易把“类型层表示”当成“运行时行为”。
面试题
Q1. 条件类型 T extends U ? X : Y 中,extends 判断的本质是什么?
- A.判断 T 运行时与 U 完全相等
- B.判断 T 能否赋给 U(结构兼容)← 正确答案
- C.判断 T 的键数量是否超过 U
- D.判断 T 是否是 U 的子类实例
答案:B。extends 做的是结构可赋值性检查,只要 T 具备 U 要求的全部形状就算通过,不要求相等,也不存在运行时比较。就像分拣机只看包裹能否塞进规格箱,不看品牌也不称重量。A 错在“运行时”,C 错在计数,D 错在把类型兼容理解成了类继承。
Q2. 下面哪个类型作为裸类型参数时会被分布式展开?
- A.联合 string | number← 正确答案
- B.对象 { a: string }
- C.元组 [string, number]
- D.字面量 "hello" 类型
答案:A。只有联合类型会被分布式条件类型拆成单件逐一判定。对象、元组、字面量都是单一类型,不会触发拆箱。就像分拣中心只拆混装箱,单件包裹直接原样过检,不额外拆解。
Q3. Exclude<"a" | "b" | "c", "a"> 的结果是什么?
- A.得到联合 "b" | "c"← 正确答案
- B.得到原联合 "a" | "b" | "c"
- C.得到 never 空类型
- D.得到类型 "a" 单例
答案:A。Exclude<T, U> 对每个成员做 T extends U ? never : T。三个成员中只有 "a" 命中 U,被替换成 never 后自动丢弃,剩下的 "b" | "c" 重新合并返回。整个过程就是“拆箱-逐件扫描-丢掉命中件-重装”。
Q4. 关于 infer R 的说法,正确的是哪一项?
- A.R 与 extends 左侧类型完全无关
- B.R 只能出现在条件类型的假分支
- C.R 从被匹配位置提取真实类型← 正确答案
- D.R 会在运行时被实例化为对象
答案:C。infer R 在 extends 的匹配模式中声明一个类型变量,TS 会根据被匹配位置的实际结构把真实类型填进 R。它只存在于编译期,且只能在真分支使用。就像检查窗口抄录包裹内说明书的内容,位置决定内容,运行时毫无踪迹。
Q5. keyof { a: number; b: string } 的结果是什么?
- A.number | string(值类型联合)
- B."a" | "b"(键名字面量联合)← 正确答案
- C.{ a; b } 的整体对象类型
- D.number 与 string 的交集类型
答案:B。keyof 取的是对象键名的联合类型,而不是值类型的联合。"a" | "b" 就是那串“货架钥匙”,后续映射类型用 in 遍历的正是这把钥匙串。A 是值的类型,C 是原对象整体,D 完全是另外的概念。
Q6. 关于分布式条件类型,下列说法正确的有?
- A.仅对裸类型参数的联合逐个展开← 正确答案
- B.结果会重新合并成联合类型← 正确答案
- C.用 [T] 包裹可关闭分布行为← 正确答案
- D.对象属性也会被逐个展开
答案:ABC。分布式展开只发生在“裸类型参数 + 联合类型”组合下,每个成员独立判型后结果重新合并;用 [T] extends [U] 把参数包进元组,就相当于给混装箱加封条,关闭拆件。对象属性属于映射类型的管辖范围,不会触发条件类型的分布式展开。
Q7. ReturnType<(x: number, y: string) => boolean> 的结果是什么?
- A.number | string 的联合
- B.参数类型组成的元组
- C.boolean 返回类型← 正确答案
- D.unknown 兜底类型
答案:C。ReturnType 内部用 F extends (...args: any[]) => infer R ? R : any,infer 放在返回位置,因此提取到 boolean。A 和 B 分别是参数相关的类型,D 是匹配失败时的兜底值,这里 F 能匹配函数形态,不会走兜底分支。
Q8. 映射类型 { [K in keyof T]: T[K] } 的变体中,可以做到哪些事?
- A.给属性加上 readonly 锁← 正确答案
- B.去掉属性的可选问号← 正确答案
- C.用 as 重映射键名← 正确答案
- D.在运行时遍历对象数据
答案:ABC。映射类型支持修饰符增减(readonly、? 的加减)和 as 键重映射,这些都是编译期类型变换。D 错在“运行时”——映射类型根本不产生运行时代码,跟遍历对象数据没有任何关系。
Q9. 防止分布式条件类型展开的正确写法是什么?
- A.用 [T] extends [U] 包一层← 正确答案
- B.把 T 转成字符串再比较
- C.使用 any 替代联合类型
- D.在 extends 前加 typeof 运算
答案:A。把裸类型参数 T 放进元组 [T],就不再是“裸”参数,[T] extends [U] 会把整个联合当成一个整体判断,从而关闭分布式展开。B、C、D 都改变了语义或引入了运行时概念,与条件类型的编译期机制相悖。
Q10. 实现 DeepPartial<T> 递归映射,下列说法正确的有?
- A.每个属性都要置为可选标记← 正确答案
- B.嵌套对象要递归调用自身← 正确答案
- C.原子类型应原样保留不递归← 正确答案
- D.仅顶层生效不处理深层嵌套
答案:ABC。DeepPartial 的核心是“逐键加 ? + extends object 判断 + 是对象就递归、否则原样保留”。它必须贯穿嵌套结构的每一层。D 描述的只改造一层的效果是普通 Partial 的行为,不是 DeepPartial。