ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Type Challenges 精讲:用 TypeScript 类型系统实现联合类型的全排列(Permutation)

Type Challenges 精讲:用 TypeScript 类型系统实现联合类型的全排列(Permutation) 示例工程【免费下载链接】type-challengesCollection of TypeScript type challenges with online judge项目地址https://gitcode.com/GitHub_Trending/ty/type-challenges点击查看免费下载type-challenges 仓库中的第 296 号中等难度题目 Permutation 要求在不借助运行时、仅靠类型系统运算的前提下把一个联合类型展开成「包含该联合类型所有排列」的元组联合。本文以该题为核心系统讲解条件类型分发distributive conditional types、递归类型与元组拼接三大核心技法并给出可一次通过仓库全部测试用例的完整实现帮助读者掌握类型级「循环 分支」的编程范式。题目速览将联合类型转换为排列数组该题目由 Naoto Ikunopandanoir 标明其难度为medium、标签为union。原题描述韩文版 / 英文版非常精炼주어진 유니언 타입을 순열 배열로 바꾸는 Permutation 타입을 구현하세요. 实现 Permutation 类型将给定的联合类型转换为排列数组。题目给出的唯一示例type perm PermutationA | B | C; // [A, B, C] | [A, C, B] | [B, A, C] | [B, C, A] | [C, A, B] | [C, B, A]也就是说输入A | B | C这样的三成员联合输出应当是 3! 6 个元组的联合覆盖全部排列。仓库为该题准备的起始模板 template.ts 只有一行占位实现type PermutationT any评测则由 test-cases.ts 中的Equal断言驱动全部用例通过才视为完成。前置知识条件类型的分发分布行为实现全排列最关键的前提是理解 TypeScript 条件类型的一个隐藏规则当条件类型T extends X ? A : B中的T是裸类型参数naked type parameter且实例化时被传入联合类型时条件类型会对联合的每个成员分别求值再把结果合并回联合。这一行为称为“分发”distribution。例如type ToArrayT T extends any ? T[] : never type R ToArrayA | B // A[] | B[]而不是 (A | B)[]正因为分发机制条件类型在类型层面具备了“遍历联合类型每个成员”的能力——这正是递归全排列的“循环入口”。本仓库的工具定义 utils/index.d.ts 中经典的UnionToIntersection也建立在该机制之上export type UnionToIntersectionU (U extends any ? (k: U) void : never) extends (k: infer I) void ? I : never它利用U extends any强制分发把每个联合成员放进函数参数位置再取交集。可以看出理解分发是打通这类 union 题目如 00055-hard-union-to-intersection、00730-hard-union-to-tuple的共同钥匙。逐步推导从单元素到完整排列第一步利用分发“拆开”联合类型先写一个最朴素的版本让分发机制为联合的每个成员产出一个单元素元组type PermutationT T extends any ? [T] : never type P PermutationA | B | C // [A] | [B] | [C]方向是对的——分发把A | B | C拆成了三个分支。但全排列还要求每个元素后面能接上“剩余元素的所有排列”即递归[A, ...PermutationB | C]第二步用ExcludeU, T构造“剩余元素”递归时需要从原始联合中去掉当前已选中的元素。若直接在分发后的分支里写ExcludeT, T此时T已被收窄为单个成员ExcludeT, T恒为空无法得到剩余元素。解决办法是用一个默认类型参数快照原始联合type PermutationT, U T T extends U ? [T, ...PermutationExcludeU, T] : neverU T在类型实例化时把完整的原始联合固化下来T extends U触发分发使T依次成为每个成员ExcludeU, T则从快照中剔除当前成员得到递归所需的“剩余联合”。仍以A | B | C为例展开后的形状是A分支[A, ...PermutationB | C]→[A, B, C] | [A, C, B]B分支[B, ...PermutationA | C]→[B, A, C] | [B, C, A]C分支[C, ...PermutationA | B]→[C, A, B] | [C, B, A]三个分支合并正是题目期望的 6 个排列元组。第三步兜底never——别让分发吞掉边界递归的终止条件藏在never上。当ExcludeU, T为空时递归调用变成Permutationnever。此时若直接写T extends U ? ... : never由于T extends ...是裸类型参数条件且T never分发规则会直接返回never——分发把never当作“空联合”处理条件类型整体得到never而不是我们想要的[]。解决方法是把T包进元组禁止分发[T] extends [never] ? [] : ...[never] extends [never]是普通的非分发元组结构比较结果为真从而正确返回[]为递归画上句号。最终实现type PermutationT, U T [T] extends [never] ? [] : T extends U ? [T, ...PermutationExcludeU, T] : never对照模板 template.ts 中的type PermutationT any将any替换为上述实现即可。用仓库测试用例逐条验证test-cases.ts 共给出 5 组断言覆盖了单元素、多元素、乱序输入、布尔与never五种场景用例输入期望输出说明1A[A]单元素联合只有一种排列2A | B | C6 个排列元组的联合标准三元素全排列3B | A | C与用例 2 相同的 6 个元组验证结果与成员书写顺序无关4boolean[false, true] | [true, false]布尔类型按true \| false参与分发5never[]空联合应返回空元组各用例对应的实现行为用例 1[T] extends [never]为假T extends U分发后只有A一个分支ExcludeA, A为空递归返回[]拼出[A]。用例 2、3集合意义上B | A | C与A | B | C是同一个联合因此无论分发顺序如何产出的 6 个元组集合完全相同Equal断言成立——这也是实现不依赖成员书写顺序的原因。用例 4boolean在严格模式下等价于true | false分发会依次产生[true, ...Permutationfalse]与[false, ...Permutationtrue]最终得到[false, true] | [true, false]。用例 5[never] extends [never]命中兜底分支直接返回[]不会因分发机制产生never。所有断言均使用 utils/index.d.ts 中基于函数参数逆变比较实现的严格相等类型Equal任何多余、缺失或顺序错误的排列都会导致编译失败验证非常严格。关键细节深挖为什么U必须通过默认参数传入type PermutationT, U T中U在实例化PermutationA | B | C时被绑定为完整联合。若去掉U改用ExcludeT, T由于分发后T已是单一成员Exclude结果恒为空排列永远无法展开。快照技巧是本题的灵魂同样适用于仓库中其他需要“记住整体、逐个消费”的类型题如 08987-medium-subsequence、21220-medium-permutations-of-tuple。为什么never判定要写[T] extends [never]裸类型参数上的条件类型对never同样会“分发”——never extends U直接得到never整个分支不可达。把T包进元组[T]后不再触发分发[never] extends [never]才能被求值为真。这一元组包裹技法也是判别类型是否为never的通用模式仓库中 01042-medium-isnever 一题正是它的直接应用。为什么结果是元组联合而不是元组每一层递归的分支[T, ...Permutation...]本身就是分发合并的结果外层分支按联合成员展开、内层递归也按剩余成员展开最终整个类型表达式的所有路径被合并成一个“排列元组”的联合。这恰好符合题面“包含所有排列的数组的联合”的语义也是类型层“枚举全部可能性”的典型表达。扩展阅读排列思想在仓库中的进阶应用掌握 Permutation 后可以继续挑战仓库中与其同源、层层递进的题目21220-medium-permutations-of-tuple输入从联合类型换成元组仍需生成全部排列但拆分与重组的目标变成元组结构通常需要配合T[number]把元组转成联合再复用本思路04260-medium-nomiwaseAllCombinations由排列放宽为组合允许任意长度、任意顺序的拼接本质是同一套「分发 快照 递归」框架00730-hard-union-to-tuple反向操作把联合类型稳定地转换成元组难点同样在于抑制never分发与保持顺序稳定00055-hard-union-to-intersection基于分发机制在函数逆变位置做交集可视为对分发行为另一面的考察。在本地仓库中验证解法本仓库根目录 README.md 提供了本地玩法克隆仓库并安装依赖后运行pnpm generate即可将题目生成为本地可玩的 TypeScript 文件之后在任何带 TypeScript 语言服务的 IDE 中打开 test-cases.ts若类型推断无红色报错即代表实现通过了全部ExpectEqual...断言。仓库本身运行在严格模式strict下见 tsconfig.base.json 与根 tsconfig.json这保证了boolean会被视为true | false这样的双成员联合用例 4 的成立依赖于此。小结Permutation 是一道“小而全”的中等难度题它只用三个语言特性——条件类型分发、递归类型、元组展开spread就完整实现了类型级的排列算法。解题过程中的三个关键决策快照原始联合、元组包裹阻断分发、Exclude逐成员消耗分别对应着类型编程中「记住状态」「处理空集」「迭代推进」的通用方法论。吃透这道题也就掌握了类型系统中编写递归算法与处理边界情况的完整套路后续挑战组合、子序列、元组排列等进阶题目都将事半功倍。赞分享示例工程【免费下载链接】type-challengesCollection of TypeScript type challenges with online judge项目地址https://gitcode.com/GitHub_Trending/ty/type-challenges点击查看免费下载相关推荐TypeScript 联合类型转交叉类型type-challenges 55 UnionToIntersection 源码级拆解TypeScript 联合类型转交叉类型type challenges 55 UnionToIntersection 源码级拆解 本篇以 type chall示例工程深度解析 TypeScript 联合类型转交叉类型type-challenges 第 55 题 UnionToIntersection 实现原理深度解析 TypeScript 联合类型转交叉类型type challenges 第 55 题 UnionToIntersection 实现原理 联合类型U示例工程掌握TypeScript高级类型UnionReplace挑战完全解析指南掌握TypeScript高级类型UnionReplace挑战完全解析指南 Type Challenges是一个专注于提升TypeScript和泛型编程能力的学示例工程上一篇计算机视觉入门指南10个实战项目带你掌握图像处理与识别技术下一篇DaoCloud公开镜像仓库同步机制解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表