type-challenges 中等题解析:用 TypeScript 类型系统实现 Lodash.without(Without\<T, U> 数组剔除)

发布时间:2026/10/2 1:53:43
type-challenges 中等题解析:用 TypeScript 类型系统实现 Lodash.without(Without\<T, U> 数组剔除) 示例工程【免费下载链接】type-challengesCollection of TypeScript type challenges with online judge项目地址https://gitcode.com/GitHub_Trending/ty/type-challenges点击查看免费下载本篇技术指南围绕 GitHub 推荐项目精选 / type-challenges 仓库中的第 5117 号中等难度题目Without关联文档questions/05117-medium-without/README.md展开。该题要求我们在纯类型层面复刻 Lodash 的without函数语义实现一个能从元组类型中剔除指定元素单个数字或数字数组的泛型WithoutT, U。读完本文你将掌握联合类型归一化 条件类型递归 元组解构的组合套路能独立解决此类数组过滤 / 剔除类型的进阶问题并为后续类似题目如Filter、Unique等打下基础。题目背景在类型层面过滤数组题目要求正如 README.md 所述题目要求实现一个泛型Implement the type version of Lodash.without,WithoutT, Utakes an ArrayT, number or arrayUand returns an Array without the elements ofU.即第一个类型参数T一个数组元组类型第二个类型参数U可以是单个数字也可以是一个数字数组返回值一个剔除掉U中所含元素之后的数组。题目给出的三个验收示例为type Res Without[1, 2], 1; // expected to be [2] type Res1 Without[1, 2, 4, 1, 5], [1, 2]; // expected to be [4, 5] type Res2 Without[2, 3, 2, 3, 2, 3, 2, 3], [2, 3]; // expected to be []三个用例依次覆盖了剔除单个元素、剔除多个元素、以及源数组与剔除集合完全重叠结果为空元组三种情况。这是验证任何解法正确性的最小测试集。难度与标签从 info.yml 可以看到该题的元数据difficulty: medium title: Without tags: union, arrayunion与array两个标签精准概括了解题的核心工具联合类型用于表达待剔除的元素集合元组递归用于逐项过滤。验收标准仓库中的测试用例仓库为每个题目都配套了可运行的测试用例。本题的测试文件位于 test-cases.ts内容如下import type { Equal, Expect } from type-challenges/utils type cases [ ExpectEqualWithout[1, 2], 1, [2], ExpectEqualWithout[1, 2, 4, 1, 5], [1, 2], [4, 5], ExpectEqualWithout[2, 3, 2, 3, 2, 3, 2, 3], [2, 3], [], ]其中Equal与Expect来自仓库的 utils/index.d.tsexport type ExpectT extends true T export type EqualX, Y (T() T extends X ? 1 : 2) extends (T() T extends Y ? 1 : 2) ? true : falseEqual采用的是函数签名比较的严格相等判定能正确区分1与number、[1]与1[]这类宽松相等无法区分的类型因此我们的解法必须在结构完全一致的意义上产出结果而不能只是可赋值。例如把Without[1, 2], 1写成number[]是无法通过Equal校验的必须精确地返回[2]。核心思路两个关键前提前提一把单个元素归一化为联合类型U可能是1这样的单个数字也可能是[1, 2]这样的数组。为了统一处理可以先定义一个辅助类型把U归一化为联合类型type ToUnionU U extends any[] ? U[number] : U当U [1, 2]时U[number]为1 | 2当U 1时直接返回1。这样无论调用方传单个数字还是数组后续判断元素是否应被剔除都只需要做一次Element extends ToUnionU的包含性检查。前提二用递归逐项扫描元组处理元组的经典手段是首元素 剩余元组的解构递归type WithoutT extends any[], U T extends [infer First, ...infer Rest] ? /* 处理 First继续处理 Rest */ : T每次递归取一个元素判断它是否命中剔除集合命中则丢弃该元素直接继续处理Rest未命中则保留该元素将其拼接在递归结果之前。当T被解构完毕匹配[]时递归终止返回[]。完整解法从朴素版到单类型版解法一借助辅助类型的朴素版type ToUnionU U extends any[] ? U[number] : U type WithoutT extends any[], U T extends [infer First, ...infer Rest] ? First extends ToUnionU ? WithoutRest, U : [First, ...WithoutRest, U] : T逐行解读T extends [infer First, ...infer Rest]如果元组至少有一个元素解构出首元素First与剩余部分RestFirst extends ToUnionU判断首元素是否属于待剔除集合联合类型天然支持这种包含判断First为联合成员之一时条件成立成立则不保留该元素返回WithoutRest, U不成立则保留返回[First, ...WithoutRest, U]当T为空元组时extends分支失败原样返回T即[]递归终止。逐个验证题目用例type Res Without[1, 2], 1 // [2] type Res1 Without[1, 2, 4, 1, 5], [1, 2] // [4, 5] type Res2 Without[2, 3, 2, 3, 2, 3, 2, 3], [2, 3] // []解法二去掉辅助类型的紧凑版也可以把归一化逻辑内联进主类型不引入额外辅助类型type WithoutT extends any[], U T extends [infer First, ...infer Rest] ? First extends (U extends any[] ? U[number] : U) ? WithoutRest, U : [First, ...WithoutRest, U] : T两种写法语义完全等价第二种更紧凑适合单类型一条龙的提交风格第一种可读性更好便于后续维护与扩展。边界情况与类型安全考量泛型约束的选择在模板 template.ts 中初始占位为type WithoutT, U any。实践上建议为T加上元组约束T extends any[]或T extends unknown[]好处有两点让类型错误在调用点更早暴露避免传入string等非数组类型时得到难以理解的推导结果让递归解构分支在类型层面更严谨。与其他数组处理题型的呼应Without是 type-challenges 数组处理系列中的一环。例如仓库中第 3062 号题 questions/03062-medium-shift 同样使用解构 递归处理元组type ShiftT any // 期望Shift[3, 2, 1] 得到 [2, 1]即丢弃首元素Shift丢弃的是固定位置首元素而Without丢弃的是满足条件的值可以看作从按位置过滤到按值过滤的进阶。掌握了Without的写法再去做Filter按类型谓词过滤、Unique去重、Drop系列都会非常顺滑。关于联合类型分发的一个提醒ToUnionU U extends any[] ? U[number] : U中使用了U extends any[]而非U extends unknown[]二者在此场景均可但要注意若U本身是联合类型例如1 | 2条件类型会对其分发此时U[number]的分支不会触发直接返回U本身语义仍然正确。若想彻底规避分发带来的意外行为也可以用[U] extends [any[]]的非分发写法效果一致且更稳。如何在本仓库验证你的解法本仓库是 TypeScript 类型体操的题库仓库utils 工具包见 utils/package.json基于 TypeScript 5.x。你可以这样验证解法将 template.ts 中的type WithoutT, U any替换为上面的完整实现保持 test-cases.ts 不动在仓库根目录运行 TypeScript 编译器对类型做检查例如pnpm exec tsc --noEmit questions/05117-medium-without/test-cases.ts若所有ExpectEqual...均通过编译即表示解法满足全部验收用例若某个Equal不成立编译器会在ExpectT extends true处报错帮助你定位是哪一种调用形态出了问题。总结WithoutT, U的解题链路非常清晰值得作为 medium 级别的范式题目反复演练步骤使用的类型能力目的归一化U条件类型 索引访问U[number]统一处理单个数字与数字数组两种入参逐项扫描TT extends [infer First, ...infer Rest]每次取出一个元素与剩余元组剔除判断First extends Union利用联合类型的包含语义判断是否命中递归拼接展开运算符[First, ...Rest]保留未命中元素丢弃命中元素递归终止空元组分支原样返回得到最终过滤结果掌握这套解构 条件 递归 展开拼接的组合拳你不仅能通过本题的 3 个测试用例还能迁移到绝大多数数组类类型挑战中。仓库中同一系列的更多题目如 questions/00459-medium-flatten、questions/05160-medium-indexof 等都是加深这一模式理解的最佳练习场。赞分享示例工程【免费下载链接】type-challengesCollection of TypeScript type challenges with online judge项目地址https://gitcode.com/GitHub_Trending/ty/type-challenges点击查看免费下载相关推荐TypeScript 类型挑战 Without用类型级编程实现 Lodash.without 的数组元素剔除medium 题解与仓库源码解析TypeScript 类型挑战 Without用类型级编程实现 Lodash.without 的数组元素剔除medium 题解与仓库源码解析 导读 本文围示例工程MOSS-Video-Preview-Base性能调优如何实现极致推理效率MOSS Video Preview Base性能调优如何实现极致推理效率 MOSS Video Preview Base是一款强大的视频理解模型通过优化配示例工程type-challenges 中等题解用 TypeScript 类型系统实现 Diff 对象差集type challenges 中等题解用 TypeScript 类型系统实现 Diff 对象差集 导读 本文深入解析 type challenges 题库第示例工程上一篇react-hot-toast错误边界设计捕获通知渲染异常下一篇猫抓3步把网页视频下载下来的资源嗅探扩展创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考