
文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载本篇是《前端精读周刊》TS 类型体操 系列的实战解读以 type-challenges Medium 难度第 41~48 题为素材逐题拆解ObjectEntries、Shift、TupleToNestedObject、Reverse、FlipArguments、FlattenDepth、BEM style string、InorderTraversal八道类型题。读完你不仅能拿到每一题可运行的最终答案更能掌握「对象/数组 ↔ 联合类型转换」「infer模式匹配」「递归与辅助泛型」「字符串模板类型」「用数组长度模拟数值运算」这五类 TS 类型编程的核心思维并理解为什么正向判断而非用null判断是 TS 递归的标准姿势。前置知识对象与数组如何转联合类型这批题目里多道题的第一步都是「把某个容器类型摊开成联合类型」思路高度统一数组转联合类型用[number]作为下标索引。[1, 2, 3][number] // 1 | 2 | 3对象转联合类型用[keyof T]作为下标索引。type ObjectToUnionT T[keyof T]这个套路在本系列前面的 247.精读《Diff, AnyOf, IsUnion...》 中已经反复出现过比如Diff借助Excludekeyof A, keyof B取 Key 差集本质都是对keyof T的运用。理解了它ObjectEntries就成功了一半。ObjectEntries实现 TS 版Object.entries实现 TS 版本的Object.entriesinterface Model { name: string; age: number; locations: string[] | null; } type modelEntries ObjectEntriesModel // [name, string] | [age, number] | [locations, string[] | null]看到这道题后的第一反应应该是如何先把对象转换为联合类型这个问题不解决就无从下手。按上面的前置知识答案呼之欲出——构造一个 Value 是[K, T[K]]结构的对象再用[keyof T]摊开type ObjectEntriesT { [K in keyof T]: [K, T[K]] }[keyof T]但这版无法通过单测ObjectEntries{ key?: undefined }原因是可选 Key 会被 TS 自动补上undefined导致key位置出现undefined。解决方法是使用映射类型修饰符-?强制把所有 Key 描述为非可选type ObjectEntriesT { [K in keyof T]-?: [K, T[K]] }[keyof T]还有最后一个单测ObjectEntriesPartialModel要过Partial会让所有值带上undefined因此还需要把 Value 中的undefined剔除// 本题答案 type RemoveUndefinedT [T] extends [undefined] ? T : ExcludeT, undefined type ObjectEntriesT { [K in keyof T]-?: [K, RemoveUndefinedT[K]] }[keyof T]这里的RemoveUndefined用[T] extends [undefined]而非T extends undefined正是为了避免联合类型分配律把T拆开逐项判断——这是本系列在 246.精读《Permutation, Flatten, Absolute...》 中详细讲解过的经典技巧[T] extends [never]同理值得记住。Shift抛弃数组第一项实现 TS 版Array.shifttype Result Shift[3, 2, 1] // [2, 1]这道题可以说是简单难度只要把第一项抛弃即可利用infer轻松实现// 本题答案 type ShiftT T extends [infer First, ...infer Rest] ? Rest : never关键点有二一是infer First会匹配到数组第一项二是...infer Rest的 rest 语法把剩余元素收集成一个新元组。注意First虽然没在结果中使用但必须存在才能让模式匹配成立——T为空数组时匹配失败返回never语义上与 JS 的shift()返回undefined不同这是类型层面合理的边界处理。Tuple to Nested Object元组递归嵌套成对象实现TupleToNestedObjectT, P其中T仅接收字符串数组P是任意类型生成一个递归对象结构type a TupleToNestedObject[a], string // {a: string} type b TupleToNestedObject[a, b], number // {a: {b: number}} type c TupleToNestedObject[], boolean // boolean元组为空时直接返回 P这道题一次用到了 5 个知识点递归、辅助类型、infer、如何指定对象 Key、PropertyKey全部知道并组合起来才能解决。首先因为返回值是递归对象递归过程中必定不断修改它因此给泛型添加第三个参数R存储这个对象并且在递归数组时从最后一个元素开始这样就能从最内层对象开始一点点把它「包起来」type TupleToNestedObjectT, U, R U /** 伪代码 T extends [...infer Rest, infer Last] */下一步是如何描述一个对象 Key之前Chainable Options例子中学到过K in Q见 244.精读《Get return type, Omit, ReadOnly...》 中的Chainable题option: K extends string, V(key: K, value: V) ChainableResult { [P in K]: V }但需要注意直接这么写会报错因为必须声明Q extends PropertyKey。最后再处理递归结束条件——T变成空数组时直接返回R// 本题答案 type TupleToNestedObjectT, U, R U T extends [] ? R : ( T extends [...infer Rest, infer Last extends PropertyKey] ? ( TupleToNestedObjectRest, U, { [P in Last]: R } ) : never )注意infer Last extends PropertyKey这种「infer 带约束」的写法TS 4.7 支持它保证Last一定是合法对象 Key同时让映射类型[P in Last]不再报错。递归过程中R从最初的U开始每剥掉数组最后一个元素就把当前累积的R包进一层新对象直到数组为空返回R。Reverse反转元组实现 TS 版Array.reversetype a Reverse[a, b] // [b, a] type b Reverse[a, b, c] // [c, b, a]这道题比上一题简单只需要用一个递归即可// 本题答案 type ReverseT extends any[] T extends [...infer Rest, infer End] ? [End, ...ReverseRest] : T思路每轮用infer End取出最后一个元素放到新元组头部剩余部分递归处理数组为空时直接返回T即[]递归自然终止。注意这里 base case 返回的是T本身而不是空数组字面量这样对[]输入也能保持类型正确。FlipArguments反转函数参数实现FlipArgumentsT将函数T的参数反转type Flipped FlipArguments(arg0: string, arg1: number, arg2: boolean) void // (arg0: boolean, arg1: number, arg2: string) void本题与上一题类似只是反转内容从数组变成了函数的参数只要用infer定义出函数的参数再利用Reverse反转一下即可// 本题答案 type ReverseT extends any[] T extends [...infer Rest, infer End] ? [End, ...ReverseRest] : T type FlipArgumentsT T extends (...args: infer Args) infer Result ? (...args: ReverseArgs) Result : never(...args: infer Args)会把整个参数列表捕获成元组Argsinfer Result捕获返回值不满足函数形态时返回never。这个「函数签名的模式匹配」在业务中非常常见例如包装函数时保留并变换原签名。FlattenDepth按指定深度打平数组实现指定深度的 Flattentype a FlattenDepth[1, 2, [3, 4], [[[5]]]], 2 // [1, 2, 3, 4, [5]]打平 2 次 type b FlattenDepth[1, 2, [3, 4], [[[5]]]] // [1, 2, 3, 4, [[5]]]深度默认为 1这道题比之前 246 期的Flatten更棘手因为需要控制打平的次数。回顾一下Flatten的递归套路T extends [infer Start, ...infer Rest]元素是数组则递归展开否则直接收集。FlattenDepth的基本想法是先实现打平一次的函数再根据Deep值递归对应次数。先实现打平一次单层的FlattenOncetype FlattenOnceT extends any[], U extends any[] [] T extends [infer X, ...infer Y] ? ( X extends any[] ? FlattenOnceY, [...U, ...X] : FlattenOnceY, [...U, X] ) : U注意这里与Flatten的区别Flatten遇到嵌套数组会递归到底...FlattenStart而FlattenOnce只展开一层...X直接展开当前元素不再递归进X内部这正是「一次打平」的语义。再实现主函数FlattenDepth。因为TS 无法实现 、- 号运算我们必须用数组长度判断与操作数组来辅助实现这与 248 期MinusOne用[length]模拟运算的思路一脉相承// FlattenOnce 见上 type FlattenDepth T extends any[], U extends number 1, P extends any[] [] P[length] extends U ? T : ( FlattenDepthFlattenOnceT, U, [...P, any] )P是递归深度计数器每递归一次就[...P, any]给数组塞一个元素下次递归时若P[length] extends U成立说明递归深度已达到U返回当前打平结果。但测试用例FlattenDepth[1, [2, [3, [4, [5]]]]], 19260817会引发超长次数递归因此需要提前终止如果打平一次后结果和原来一样已经是平的就不用继续递归了此时用FlattenOnceT extends T判断// 本题答案 // FlattenOnce 见上 type FlattenDepth T extends any[], U extends number 1, P extends any[] [] P[length] extends U ? T : ( FlattenOnceT extends T ? T : ( FlattenDepthFlattenOnceT, U, [...P, any] ) )三行判断的优先级先看深度是否达标再看本轮打平是否无变化提前终止最后才继续递归。这种「递归 提前终止」的双保险是类型递归面对极端输入的通用防御手段。BEM style string模板字符串的笛卡尔积实现BEM函数完成其规则拼接ExpectEqual BEMbtn, [], [small, medium, large], btn--small | btn--medium | btn--large 之前了解了通过下标将数组或对象转成联合类型这里还有一个特殊情况在字符串模板中通过下标方式声明每一项会自动做笛卡尔积生成新的联合类型。最简单的写法type BEMB extends string, E extends string[], M extends string[] ${B}__${E[number]}--${M[number]}但这样没有考虑项不存在的情况当E或M为空数组时E[number]是never整个字符串模板会坍缩为never导致结果错误。解决思路是创建一个SafeUnion辅助类型当传入值不存在时返回空字符串安全跳过该段拼接type IsNeverTValue TValue[] extends never[] ? true : false type SafeUnionTUnion IsNeverTUnion extends true ? : TUnionIsNever的判断技巧也来自本系列 247 期的 IsNever 题TValue[] extends never[]利用了never[]的特殊性来判断TValue是否为never。最终代码// 本题答案 // IsNever, SafeUnion 见上 type BEMB extends string, E extends string[], M extends string[] ${B}${SafeUnion__${E[number]}}${SafeUnion--${M[number]}}当E []时__${never}通过SafeUnion变成最终得到btn--small | btn--medium | btn--large这类只带修饰符的结果当M []时同理。这比「直接用字面量判断数组是否为空」更通用因为它作用于任意联合类型。InorderTraversalTS 版二叉树中序遍历实现 TS 版二叉树中序遍历const tree1 { val: 1, left: null, right: { val: 2, left: { val: 3, left: null, right: null }, right: null, }, } as const type A InorderTraversaltypeof tree1 // [1, 3, 2]先回忆 JS 版中序遍历的实现function inorderTraversal(tree) { if (!tree) return [] return [ ...inorderTraversal(tree.left), tree.val, ...inorderTraversal(tree.right) ] }对 TS 来说递归的实现方式有一点点不同即通过extends TreeNode来判定它不是 Null从而递归// 本题答案 interface TreeNode { val: number left: TreeNode | null right: TreeNode | null } type InorderTraversalT extends TreeNode | null [T] extends [TreeNode] ? ( [ ...InorderTraversalT[left], T[val], ...InorderTraversalT[right] ] ) : []你可能会问为什么不能像 JS 一样用null做判断呢type InorderTraversalT extends TreeNode | null [T] extends [null] ? [] : ( [ // error ...InorderTraversalT[left], T[val], ...InorderTraversalT[right] ] )这么写会发现 TS 抛出异常因为 TS 不能确定T此时符合TreeNode类型虽然排除了null但在条件分支里 TS 没有「负向收窄」能力所以要对T执行属性访问时必须采用正向判断extends TreeNode这正是 TS 类型编程与 JS 运行时代码在思维上的关键差异。同时注意[T] extends [TreeNode]的外层元组包裹用于规避联合类型分配律可能带来的意外展开。总结这些类型挑战题目需要灵活组合 TS 的基础知识点才能破解本批八题反复用到的核心能力包括如何操作对象增减 Key、只读修饰符-?、keyof与T[keyof T]下标访问以及用as变形 Key见 250 期 Flip。递归与辅助类型用「泛型默认值」存储累积状态R、P、U等这是本系列多个题目的通用套路最早可追溯到 243 期 Pick 的泛型默认值讲解 与 246 期 Flatten 的Result参数。infer知识点[...infer Rest, infer End]、(...args: infer Args) infer Result、infer Last extends PropertyKey用于从元组、函数签名、字符串中提取子结构。联合类型如何从对象或数组生成联合类型[keyof T]、[number]字符串模板与联合类型的笛卡尔积关系以及用[T] extends [...]包裹规避分配律。正如本系列 247 期总结 所言TS 是一门编程语言而非简单的描述或修饰符很多复杂类型问题要动用逻辑思维来实现。建议读者拿到题目先自己尝试再对照本文答案逐步推演反复训练后自然能养成「TS 思维」。本文解读基于仓库文档 TS 类型体操/249.精读《ObjectEntries, Shift, Reverse...》.md该系列完整目录见仓库 readme.md 的「TS 类型体操」一节可以按 243~252 的顺序循序渐进练习。赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐cuda-samples 实战解析用 simpleCUFFT 与 cuFFT 两种 Plan API 实现一维 FFT 频域卷积cuda samples 实战解析用 simpleCUFFT 与 cuFFT 两种 Plan API 实现一维 FFT 频域卷积 导读 本篇技术指南以 cud文档技术博客教程notepad-- 深度拆解Qt5 与 QScintilla 如何实现跨平台文本编辑与文件对比notepad 深度拆解Qt5 与 QScintilla 如何实现跨平台文本编辑与文件对比 在 macOS 上打开 GBK 文件满屏乱码、2G 日志根本打不开文档技术博客教程前端精读周刊前端内存管理实践前端精读周刊前端内存管理实践 引言 你是否曾遇到过网页随着使用时间增长而变得越来越卡顿是否在开发复杂单页应用时明明代码逻辑没问题却总是出现莫名其妙的性能文档技术博客教程上一篇进阶指南React Native Swipe List View 的左右滑动动作系统下一篇Python数据处理的终极指南reader.py模块深度解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考