
TS 类型体操 Medium 17~24 题完整拆解从数字绝对值到联合全排列的 8 题递归推导指南【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址: https://gitcode.com/GitHub_Trending/we/weekly本文精读 type-challenges 题库 Medium 难度第 17~24 题共 8 道绝对值、对象追加属性、字符串转联合、字符串长度、对象合并、驼峰转横线、数组扁平化与联合类型全排列。按技术难度从低到高逐题走通「题目 → 第一反应 → 破局关键 → 逐步展开 → 验证复盘」五步带你吃透「带默认值的累加泛型 换域匹配 元组包裹 never」这套递归推导套路并汇总多写法取舍表与高频陷阱。本章题目地图先看全局下面这 8 道题按技术难度递增排列而非题号顺序难度越高越靠前需要垫底的推导能力。题目核心技术点难度Absolute 数字绝对值模板字面量换域 infer抠前缀★AppendToObject 对象追加属性映射类型 / 交叉类型★★StringToUnion 字符串转联合字符串拆分infer 联合累积★★★LengthOfString 字符串长度累加泛型 [length]取值★★★Merge 对象合并keyof并集遍历 条件索引★★★★KebabCase 驼峰转横线内置Lowercase判断 前缀剥离助手★★★★Flatten 数组扁平化元组解构infer 递归自我复用★★★★★Permutation 联合全排列分配律 元组包裹never★★★★★逐题精读按技术难度递增说明新语法点「用到时再讲」首次出现处都配了代码与解释不集中在开头。题 1Absolute 数字绝对值先换到字符串域再匹配符号【题目】type Test -100; type Result AbsoluteTest; // 期望 100【第一反应】想直接用T extends ...判断正负。但number类型本身不携带「负号字符」这个信息——-100在类型层面只是个数字你没法对它的符号做字符串匹配。卡点数字域里没有「符号」这个属性可抓。【破局关键】模板字面量类型能把类型「翻译」到字符串域${T}会把数字字面量变成对应字符串。一旦落到字符串域infer前缀匹配-${infer R}就能抠出负号后面的部分。这是「先换域、再匹配」的思路。【逐步展开】// T -100 ${T} // 先转字符串域 → -100 -100 extends -${infer R} // 命中负号前缀抠出 R 100【验证与复盘】// 最终答案先转字符串域再匹配开头的负号 type AbsoluteT extends number ${T} extends -${infer R} ? R : ${T}验证Absolute-100得100Absolute5走else分支得5两个用例都命中预期即可。✅题 2AppendToObject 对象追加属性交叉类型 vs 映射类型【题目】type Base { id: 1 } type Result AppendToObjectBase, value, 4 // 期望 { id: 1, value: 4 }【第一反应】想把新属性「拼上去」。TS 没有运行时的对象展开只能靠映射类型重新描述整个对象。卡点如何在不丢原属性的前提下把一对Key/Value塞进去。【破局关键】两条路。其一是交叉类型把「原对象」与「新增键值」直接交叉TS 自动合成其二是纯映射类型用keyof T | U把新旧 Key 并成一个遍历范围再对每个 Key 做条件判断决定它取哪个值。【逐步展开】// 思路一交叉 —— Base { value: 4 } 被 TS 自动合成 { id: 1, value: 4 } // 思路二映射 keyof 并集keyof Base | value 展开为 id | value // key id → 不在 U(value) 里 → 回退取 Base[id] 1 // key value → 命中 U → 取 V 4【验证与复盘】// 写法一交叉类型最短 type AppendToObjectObj, Key extends string, Value Obj { [K in Key]: Value } // 写法二纯映射类型输出形状精确可控 type AppendToObjectT, U extends number | string | symbol, V { [K in keyof T | U]: K extends U ? V : T[ExcludeK, U] }验证对{ id: 1 }追加value, 4两种写法都得到{ id: 1, value: 4 }。✅题 3StringToUnion 字符串转联合两种收拢递归结果的方式【题目】type Test 123; type Result StringToUnionTest; // 期望 1 | 2 | 3【第一反应】想把字符串「拆开」。TS 类型没有split但可以用字符串模板拆分T extends \${infer F}${infer R}里第一个infer 抓首字符第二个抓剩余全部字符。卡点字符分散在递归各层里怎么把它们重新「收拢」成一个联合类型【破局关键】两种收拢方式。一是联合累积泛型声明第二个泛型P默认值never每层把首字符并进P | F——never在联合里是「空占位」并入不污染结果。二是隐式拼接不存中间态直接F | 递归结果靠联合类型天然把never吸收掉。累积式的套路在上一轮 Medium 9~16 题 的ReplaceAll里已初见雏形。【逐步展开】// 走隐式拼接以 123 为例 StringToUnion123 // 拆出 F1 → 1 | StringToUnion23 // 拆出 F2 → 1 | 2 | StringToUnion3 // 拆出 F3 → 1 | 2 | 3 | StringToUnion // 不匹配模板 → never被联合吸收 // 最终 1 | 2 | 3【验证与复盘】// 写法一累积泛型中间态显式 type StringToUnionT, P never T extends ${infer F}${infer R} ? StringToUnionR, P | F : P // 写法二隐式拼接代码最简 type StringToUnionT T extends ${infer F}${infer R} ? F | StringToUnionR : never验证StringToUnion123得1 | 2 | 3展开到空串返回never且不影响结果。✅题 4LengthOfString 字符串长度用累加泛型边拆边存【题目】type Result LengthOfStringabc; // 期望 3【第一反应】字符串长度类型层没有.length但数组有——[a,b,c][length]返回3。思路是把abc变成[a,b,c]再读它的[length]。卡点这个「边拆边存」的数组要存在哪个类型变量里类型递归不像函数那样有闭包变量可写。【破局关键】带默认值的累加泛型就是类型层的「可变变量」N extends any[] []初始为空数组每递归一层把首字符 push 进去[...N, F]。字符串拆完后读N[length]。这跟函数递归里「用参数传递累加器」是同构的。【逐步展开】LengthOfStringabc // 拆出 Fa → LengthOfStringbc, [a] // 拆出 Fb → LengthOfStringc, [a,b] // 拆出 Fc → LengthOfString, [a,b,c] // 不再匹配模板 → 返回 [a,b,c][length] 3【验证与复盘】// S剩余待拆字符串N已收集的字符数组默认空 type LengthOfStringS, N extends any[] [] S extends ${infer F}${infer R} ? LengthOfStringR, [...N, F] : N[length]验证LengthOfStringabc得3边界用例LengthOfString得0均通过。✅题 5Merge 对象合并keyof 并集遍历加双重条件收敛【题目】type Foo { name: string; age: string } type Coo { age: number; sex: string } type Result MergeFoo, Coo; // 期望 { name: string, age: number, sex: string }【第一反应】「合并」两字让人想写运行时展开但类型层没有对象合并运算符只能重新描述一个对象Key 覆盖两边所有 Key冲突时后者优先。卡点keyof Foo | keyof Coo展开后TS 并不天然知道某个 Key 来自哪边直接Foo[K]可能索引到不存在的键。【破局关键】把keyof A | keyof B当成遍历范围值位置用条件索引决定来源先判K extends keyof B后者优先命中取B[K]否则再判K extends keyof A命中取A[K]。两套条件保证K至少存在于一个对象中规避非法索引。【逐步展开】// 遍历范围keyof Foo | keyof Coo name | age | sex // Kname → 不在 keyof Coo在 keyof Foo → Foo[name] string // Kage → 在 keyof Coo后者优先 → Coo[age] number // Ksex → 在 keyof Coo后者优先 → Coo[sex] string // 最终 { name: string, age: number, sex: string }【验证与复盘】// K并集里的单个键B 优先于 A type MergeA extends object, B extends object { [K in keyof A | keyof B]: K extends keyof B ? B[K] : (K extends keyof A ? A[K] : never) }验证三个 Key 各落正确分支冲突的age取B的number符合「后者优先」。✅题 6KebabCase 驼峰转横线内置 Lowercase 判大小写加助手剥横线【题目】type Result KebabCaseFooBarBaz; // 期望 foo-bar-baz【第一反应】逐字符处理遇大写转小写并前面补-小写原样。卡点有两个——一是类型层怎么判断「这是大写」二是递归拼出来的结果开头会多一个-首个字符也是大写得有人把它去掉。【破局关键】判断大小写靠内置工具类型Lowercase若LowercaseF extends F成立说明转小写后没变化、原本就是小写否则就是大写。开头多余的-交给一个前缀剥离助手S extends \${infer Rest}命中- 开头就返回余下部分。【逐步展开】KebabCaseFooBarBaz, // FF 大写 → 累积 ${}-f → KebabCaseooBarBaz, -f // Fo 小写 → 累积 ${-f}o → KebabCaseoBarBaz, -fo // ……逐字符推进遇大写前插 - // 结束时 U -foo-bar-baz // RemoveFirstHyphen-foo-bar-baz → foo-bar-baz【验证与复盘】// S剩余待转字符串U累积结果默认空串 type KebabCaseS, U extends string S extends ${infer F}${infer R} ? ( LowercaseF extends F ? KebabCaseR, ${U}${F} : KebabCaseR, ${U}-${LowercaseF} ) : RemoveFirstHyphenU // 剥离首个横线的助手 type RemoveFirstHyphenS S extends -${infer Rest} ? Rest : S验证KebabCaseFooBarBaz得foo-bar-baz首字母大写导致的开头-被助手正确去掉。✅题 7Flatten 数组扁平化递归自我复用拍到底【题目】type Result Flatten[1, 2, [3, 4], [[[5]]]]; // 期望 [1, 2, 3, 4, 5]【第一反应】一层层拍平。用元组解构infer[infer Start, ...infer Rest]把首元素和剩余分开。卡点首元素本身还可能「套了多层」如[[[5]]]一次...Start展不开到底怎么保证彻底拍平【破局关键】让递归自我复用首元素是数组时不要只展一层而是直接...FlattenStart——让同一个类型继续把Start拍到底再把结果铺进累加器。带默认值的累加泛型Result负责收集所有已拍平的元素。【逐步展开】Flatten[1, 2, [3, 4], [[[5]]]] // Start1(非数组) → Flatten[2,[3,4],[[[5]]]], [1] // Start2(非数组) → Flatten[[3,4],[[[5]]]], [1,2] // Start3,4 → Flatten[[[5]]], [1,2,...Flatten[3,4]] → [1,2,3,4] // Start[[5]](数组) → Flatten[], [1,2,3,4,...Flatten[[[5]]]] → 继续拍 [[5]]→[5] // 最终 [1, 2, 3, 4, 5]【验证与复盘】// T待拍平数组Result已拍平结果默认空 type FlattenT extends any[], Result extends any[] [] T extends [infer Start, ...infer Rest] ? Start extends any[] ? FlattenRest, [...Result, ...FlattenStart] : FlattenRest, [...Result, Start] : Result验证上例展开到[1,2,3,4,5]注意...FlattenStart的...不能省否则整个子数组会被当成单个元素塞进去。✅题 8Permutation 联合全排列分配律加元组包裹 never 终止【题目】type perm PermutationA | B | C; // 期望 [A,B,C] | [A,C,B] | [B,A,C] | [B,C,A] | [C,A,B] | [C,B,A]【第一反应】排列要「每个元素轮流当排头」这正是联合类型分配律的强项条件类型里的T若是联合T extends U ? A : B会先把T拆开逐项判断、再用|合并结果——这一点在 Easy 难度的 Exclude 里已验证过。卡点有两个题目只给了一个泛型分配律需要「当前项 全体候选」两个角色递归到底时输入变never而裸T extends never对never有分配特例会把判断直接吞掉。【破局关键】造第二个泛型U默认等于T让T当前排头对U候选全集做分配用ExcludeU, T每次剔除已用元素剩never即终止。终止判断必须写成元组包裹[T] extends [never]把T塞进元组就阻断了分配行为never才能被正常判定联合类型也不会在这里被提前分发。infer的底层原理可回看 《Typescript infer 关键字》。【逐步展开】PermutationA | B | C // 对 U 分配拆成三项 PermutationA,A|B|C | PermutationB,... | PermutationC,... // 展开第一项用 Exclude 剔除自身 A [A, ...PermutationB | C] // 再展开剔除 B [B, ...PermutationC] | [C, ...PermutationB] // 展开到叶子 [C, ...Permutationnever] → 命中 [T] extends [never]返回 [] // 逐层拼回 → [A,B,C] 等 6 种排列【验证与复盘】// T当前排头U候选全集默认等于 T type PermutationT, U T [T] extends [never] ? [] : T extends U ? [T, ...PermutationExcludeU, T] : []验证PermutationA|B|C展开出 6 种排列全命中若把[T] extends [never]误写成T extends never结果是never而非[]排列直接丢失。✅多写法对比与取舍把前面出现过的多种写法收拢成一张表方便你在实际项目里按场景选型写法优点缺点适用场景交叉类型Obj {K:V}AppendToObject代码最短保留原对象全部修饰符输出是交叉类型部分工具类型不接受交叉形状只合成对象、不关心内部形状映射类型 keyof并集AppendToObject纯映射输出形状精确可控需手写Exclude收敛键略长需要精确对象形状、被其他工具类型消费累积泛型PStringToUnion中间态显式、可读性好多一个类型参数结果需跨层传递、后续还要加工隐式联合拼接StringToUnion代码最简、无额外参数依赖never被吸收初看略隐晦结果就是联合、无后续加工元组包裹[T] extends [never]Permutation正确判定never、阻断分配比裸判断多一层括号递归终止、判断never/空裸判断T extends never写法最直白对never触发分配特例、直接吞成never基本不建议用于递归终止五个高频陷阱错误写法与正确写法陷阱 1裸判断never错误写法type PT, U T T extends never ? [] : ...后果T never时触发分配特例整体返回never排列/递归结果丢失正确写法[T] extends [never] ? [] : ...陷阱 2忘记展开子数组错误写法FlattenRest, [...Result, FlattenStart]后果[FlattenStart]把子数组当单个元素塞进Result得到嵌套而非拍平正确写法[...Result, ...FlattenStart]陷阱 3合并漏掉非法索引收敛错误写法{ [K in keyof A | keyof B]: K extends keyof B ? B[K] : A[K] }后果对「只在 A」或「只在 B」的键另一侧A[K]/B[K]索引到不存在的键正确写法K extends keyof A ? A[K] : never用双条件收敛陷阱 4KebabCase 开头横线残留错误写法直接返回累积结果U不剥离首个-后果FooBarBaz得-foo-bar-baz首字母大写导致开头多一个横线正确写法末尾接RemoveFirstHyphenU剥掉首个-陷阱 5数字绝对值在数字域匹配错误写法T extends -number ? ...之类对符号做类型匹配后果number类型不携带「负号字符」信息无法按符号匹配正确写法先${T}转字符串域再-${infer R}抠前缀收尾提炼三条可复用方法论把这 8 道题的方法论压成三条后面遇到同族题目可以直接套用类型层的「可变变量」就是带默认值的泛型字符串拆、数组拍平、结果累积本质都是把中间态挂在 []/ never/ 的默认泛型上逐层推进等价于函数递归用参数传累加器。换域再匹配number用${T}进字符串域、string用${infer F}${infer R}拆字符、对象用keyof进键域——不同「域」之间靠模板字面量、[length]、keyof切换而infer是贯穿各域的通用取件工具。never要刻意绕过递归终止用[T] extends [never]而非裸T extends never联合累积用never起点零成本起步再靠元组包裹阻断分配特例。想继续打磨手感下一批可看 Medium 25~32 题Diff、IsNever、IsUnion同族的infer拼装思路还能在 Medium 63~68 题Unique、MapTypes 里复用。落款本文整理自前端精读周刊「TS 类型体操」系列原题出自 type-challenges 题库 Medium 难度第 17~24 题。类型体操的核心不是背语法而是把类型当一门语言来编程——多拆、多推、多练推导手感自然会长出来。【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址: https://gitcode.com/GitHub_Trending/we/weekly创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考