- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
导读
OmitByType是 type-challenges 题库中的一道中等难度(medium)对象类型体操题,编号 2852,收录于仓库 questions/02852-medium-omitbytype/。它的核心诉求与内置工具类型Omit<T, K>截然不同:后者按键名集合剔除属性,而OmitByType要求按值类型剔除属性——凡值类型可赋值给U的键全部过滤掉,其余键原样保留。读完本文你将掌握"键重映射(key remapping)+ 条件类型"这一对象过滤套路,能够独立写出通过全部测试用例的题解,并理解它与姊妹题PickByType之间的镜像关系。
一、题目原文与核心目标
题面原文(摘自 README.md)非常精简:
From
T, pick a set of properties whose type are not assignable toU.
即:从对象类型T中,挑出值类型不可赋值给U的那些属性,组成新的对象类型。
题面给出的示例:
type OmitBoolean = OmitByType<{ name: string count: number isReadonly: boolean isEnable: boolean }, boolean> // { name: string; count: number }示例中isReadonly与isEnable的值类型是boolean,可赋值给参数U = boolean,因此被剔除;name: string与count: number不可赋值给boolean,得以保留。
题目的难度、标签与元信息记录在 info.yml 中:
difficulty: medium title: OmitByType tags: object author: github: jiangshanmeta name: jiangshan related: 2595可以看到它属于object(对象)标签系列,作者是 jiangshan,并且官方将其与 2595 号题PickByType标记为关联题目(related)。这与本仓库的目录组织一致:每个题目一个独立目录,包含README.md、template.ts(待补全的模板)、test-cases.ts(官方测试用例)与info.yml(元信息),题库总入口见根目录 README.md。
二、入手前的关键区分:Omit vs OmitByType
在动手前,必须先厘清它与内置工具类型Omit<T, K>的本质差异:
Omit<T, K>的第二个参数是键名(字面量类型的联合),剔除依据是"键名在不在 K 里"。本仓库对应的手写练习见 questions/00003-medium-omit/。OmitByType<T, U>的第二个参数是值类型,剔除依据是"T[K]是否可赋值给U"。它不关心键叫什么名字,只关心值的类型形态。
这种差异也决定了实现思路:内置Omit只需一次Exclude<keyof T, K>拿到保留键集合,再交给Pick;而OmitByType必须先按值类型筛选键——这正是本题的考点所在。
三、模板文件与官方测试用例
题目起点是 template.ts,内容仅为占位实现:
type OmitByType<T, U> = any我们后续的解答就是要替换掉这个any,使其通过 test-cases.ts 中的全部断言。官方测试用例原文如下:
import type { Equal, Expect } from '@type-challenges/utils' interface Model { name: string count: number isReadonly: boolean isEnable: boolean } type cases = [ Expect<Equal<OmitByType<Model, boolean>, { name: string, count: number }>>, Expect<Equal<OmitByType<Model, string>, { count: number, isReadonly: boolean, isEnable: boolean }>>, Expect<Equal<OmitByType<Model, number>, { name: string, isReadonly: boolean, isEnable: boolean }>>, ]三个用例分别以boolean、string、number作为过滤类型U,覆盖了"多个键被剔除""单个键被剔除"等场景。其中Equal与Expect来自工作区依赖包@type-challenges/utils,其定义位于 utils/index.d.ts:Expect<T extends true> = T用于强制类型恒等于true,而Equal通过一对函数签名的条件类型比较实现严格的类型等价判定,因此解答必须做到结构逐字一致(例如键的顺序、可选性、readonly 修饰符),不能只满足"可赋值"这种宽松关系。
四、题解一:先筛键、再 Pick(经典 Exclude 思路)
最直观的思路分两步:先找出"值类型可赋值给U"的键并剔除,得到保留键的联合;再用内置Pick<T, K>从原对象中取出这些键。
第一步需要用到条件类型 + 映射类型的键查询技巧。对keyof T中的每个键K,判断T[K] extends U:
type KeysNotAssignableTo<T, U> = { [K in keyof T]: T[K] extends U ? never : K }[keyof T]这段代码的含义是:把T逐键映射成一个新对象,值为never或键名K,再通过索引访问[keyof T]把所有值取并集。由于never在联合类型中会被自动吸收,最终结果就只剩下"不可赋值给U"的键名的联合。
拿到保留键后,交给内置Pick即可:
type OmitByType<T, U> = Pick<T, KeysNotAssignableTo<T, U>>以U = boolean为例:isReadonly、isEnable两个键被映射为never,KeysNotAssignableTo得到'name' | 'count',Pick<T, 'name' | 'count'>便输出{ name: string; count: number },与题面示例及第一个测试用例完全吻合。
五、题解二:键重映射(key remapping)一步到位
TypeScript 4.1 引入的键重映射(as子句)允许在映射类型中改写键名,把键映射为never即可从结果类型中删除该键。基于此,可以直接在一个映射类型内完成筛选,无需先算键联合:
type OmitByType<T, U> = { [K in keyof T as T[K] extends U ? never : K]: T[K] }对于每个键K:若T[K] extends U成立,则把键重映射为never(键被删除);否则保留键名K并携带原值类型T[K]。这套写法更紧凑,也是社区中最常见的解答。从语法机制看,它与题解一在语义上等价——都是"保留值类型不可赋值给U的键",只是把"先算键联合、再 Pick"的两步压缩进了一次映射。
对照template.ts中type OmitByType<T, U> = any的占位,将any替换为上述任一实现即可让测试用例通过。
六、深入理解:extends判定与字面量类型陷阱
理解"不可赋值(not assignable)"的判定边界是本题的关键。T[K] extends U走的是 TypeScript 的结构可赋值性规则:
- 宽类型匹配:
boolean类型属性会被U = boolean命中(如示例中的两个布尔字段); - 字面量类型也属于可赋值:若某属性的值是字面量类型
true,由于true extends boolean成立,它同样会被U = boolean剔除——这正是题面用 "assignable" 而非 "equal" 措辞的原因,也是与"按精确类型匹配"类题目(如按Equal严格比较的题)的关键区别; - 联合类型按整体判定:
string | number这类联合是否命中U,取决于联合整体能否赋值给U(需要每个成员都可赋值),而不是按成员逐个拆分。
从 test-cases.ts 的用例可以看出,官方测试只覆盖了三种原始类型作为U,并未覆盖字面量或联合类型边界;在自行验证扩展用例时,可以把上述边界作为补充自测点,例如:
type Case = OmitByType<{ a: true, b: boolean, c: string }, boolean> // 结果应为 { c: string },因为 true 与 boolean 都可赋值给 boolean七、姊妹题对比:OmitByType 与 PickByType
本题的关联题 questions/02595-medium-pickbytype/ 是PickByType——保留可赋值给U的属性,恰好是本题的反向操作:
From
T, pick a set of properties whose type are assignable toU.
其示例与本题共用同一个Model形状,只是结果互补:
type OnlyBoolean = PickByType<{ name: string count: number isReadonly: boolean isEnable: boolean }, boolean> // { isReadonly: boolean; isEnable: boolean; }对比两者的 test-cases.ts 与本题 test-cases.ts 可以发现:对同一Model,PickByType<Model, boolean>得到{ isReadonly: boolean; isEnable: boolean },而OmitByType<Model, boolean>得到{ name: string; count: number },两者键集合互为补集。实现上只需要把条件分支对调:
// PickByType:可赋值则保留键 type PickByType<T, U> = { [K in keyof T as T[K] extends U ? K : never]: T[K] } // OmitByType:可赋值则删除键 type OmitByType<T, U> = { [K in keyof T as T[K] extends U ? never : K]: T[K] }把这两道题放在一起练习,能一举掌握"按类型过滤键"的正反两套写法。
八、如何在本仓库本地验证题解
本仓库采用 pnpm workspace 管理(见根目录 package.json 中的pnpm@8.12.1与@type-challenges/utils: workspace:*),测试依赖包源码位于 utils/。本地验证步骤:
- 克隆仓库后在根目录执行
pnpm install(需 Node 环境与 pnpm); - 编辑 template.ts,把
type OmitByType<T, U> = any替换为上文任一实现; - 用 TypeScript 编译器检查测试文件,例如
npx tsc --noEmit questions/02852-medium-omitbytype/test-cases.ts——若类型定义正确,Expect<Equal<...>>全部满足约束,编译无错误即通过。
仓库根目录 tsconfig.json 与 tsconfig.base.json 已配置好编译环境;也可以直接在题目的线上 playground 地址(README 头部徽章链接)粘贴解答运行。
九、小结
OmitByType的核心价值在于把"Omit"的维度从键名扩展到值类型,其标准解法依赖两个 TypeScript 特性:条件类型T[K] extends U完成可赋值性判定,映射类型的键重映射as ... never完成键的剔除。两种实现(先Exclude式筛键再Pick、或一步键重映射)在语义上等价,读者可按习惯选用。若再与 PickByType 成对练习,即可完整掌握对象类型按值类型过滤的整套思路,为后续更复杂的对象型挑战(如按嵌套结构过滤、按修饰符过滤等)打下基础。
- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
相关推荐
Element UI(Vue 2.0)Pagination 分页组件完全指南:布局、事件与源码实现剖析
Element UI(Vue 2.0)Pagination 分页组件完全指南:布局、事件与源码实现剖析 当单页需要展示的数据量过大时,使用分页(Paginati
示例工程ant-design-vue Image 图片组件完全指南:从基础展示到可控预览的实现原理与实战
ant design vue Image 图片组件完全指南:从基础展示到可控预览的实现原理与实战 可预览的图片组件是 ant design vue 数据展示体系
示例工程CairoSVG错误处理与调试:解决常见SVG转换问题的完整指南
CairoSVG错误处理与调试:解决常见SVG转换问题的完整指南 CairoSVG是一款强大的SVG转换工具,能够将矢量图像精准转换为多种格式。然而在实际使用过
图形学开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考