Omit<T, K> 是一个内置工具类型,它创建的对象类型包含 T 的所有属性,但去掉 K 中列出的键。最典型的用法:User 类型带有 password,而它的公开版本不带。
PublicUser 会和 User 保持同步:给 User 加一个字段,PublicUser 里也会出现,而 password 始终被排除在外。Omit 只作用于类型;真正从对象上删掉属性的,是 toPublic 里的解构。
移除多个键
第二个参数是一个联合类型,所以需要移除几个键,就用 | 列出几个:
“去掉服务器生成字段的类型”是实际代码中最常见的 Omit 用法:只定义一个模型类型,再用 Omit 推导出创建表单和 API 请求所需的输入结构。
Omit 是如何定义的
Omit 由另外两个工具类型组合而成:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
从里往外读:keyof T 是所有键组成的联合类型,Exclude<keyof T, K> 从中去掉不需要的键,Pick 保留剩下的键。所以 Omit<User, "password"> 就是 Pick<User, "id" | "name" | "email">。由于 Pick 是对 T 的键做映射,剩余属性上的 readonly 和 ? 修饰符都会保留。
注意这里的约束:是 K extends keyof any,而不是 K extends keyof T。下一节就来讲这个细节。
Omit 不检查键是否存在
keyof any 是 string | number | symbol,所以 Omit 接受任何键,包括 T 根本没有的键。拼写错误照样能编译,只是什么也没移除:
哪里都没有报错,密码也随 JSON 一起发了出去。这种宽松的约束是 TypeScript 3.5 加入 Omit 时有意为之的。团队给出的理由是:在泛型代码中,要移除的键常常并不来自 T(一个合并两个对象的函数,可能会把剩余参数的类型写成 Omit<T1, keyof T2>);Exclude 对第二个参数同样没有约束;而且加上约束会破坏各个项目里大量已有的自定义 Omit 类型。在日常代码中,更严格的版本可以捕获这类拼写错误:
index.ts(10,36): error TS2344: Type '"pasword"' does not satisfy the constraint 'keyof User'.
index.ts(11,7): error TS2741: Property 'password' is missing in type '{ id: number; name: string; }' but required in type 'PublicUser'.
第一个错误才是关键:键会根据 keyof User 进行检查。第二个错误是第一个错误的连带结果,因为什么都没移除,PublicUser 仍然要求有 password。把拼写改成 "password",代码就能编译通过。很多代码库会在共享的类型文件里定义一次,然后用它代替 Omit。
覆盖属性类型
接口在继承另一个接口时,不能把某个属性改成不兼容的类型:interface ApiUser extends User { id: string } 会失败,报错 error TS2430: Interface 'ApiUser' incorrectly extends interface 'User'。先用 Omit 去掉这个属性,再重新声明它:
如果用类型别名,同样的写法是交叉类型:type ApiUser = Omit<User, "id"> & { id: string }。没有 Omit 的话,交叉类型 User & { id: string } 会让 id 的类型变成 number & string,也就是 never。
联合类型上的 Omit
Omit 不是分配式的。作用在联合类型上时,它从整个联合类型的 keyof 出发,而这里只包含所有成员共有的键,因此各成员自己的属性都会丢失:
index.ts(8,39): error TS2353: Object literal may only specify known properties, and 'radius' does not exist in type 'NewShape'.
NewShape 变成了 { kind: "circle" | "square" }。要对每个成员分别执行 Omit,就把 Omit 放进一个分配式条件类型里:
T extends unknown ? ... : never 永远为真;它存在的唯一目的,是让条件类型分配到联合类型的每个成员上。条件类型页面解释了其中的原理。只要你要 Omit 的类型是可辨识联合,就用 DistributiveOmit。
移除嵌套属性
Omit 只看顶层的键,所以 Omit<Settings, "editor.wordWrap"> 什么也不会移除(而且如上所示,也不会报错)。要去掉下一层的属性,先移除父属性,再用它自己的 Omit 把它加回来:
Settings["editor"] 是一个索引访问类型:它指代嵌套的对象类型,让 Omit 可以作用在它上面。
在运行时删除属性
Omit 只改变类型。要得到一个不含这些属性的对象,可以用解构把它们去掉,或者写一个返回类型为 Omit 的小型泛型辅助函数:
这个辅助函数把 K 约束为 keyof T,所以拼错的键会在调用处报错。复制是浅拷贝:嵌套对象仍与原对象共享。
Omit、Pick 与 Exclude 对比
| 作用于 | 保留 | 示例 | 结果 | |
|---|---|---|---|---|
Omit<T, K> | 对象类型 | 除 K 以外的所有属性 | Omit<User, "password"> | { id; name; email } |
Pick<T, K> | 对象类型 | 只保留 K 中的属性 | Pick<User, "id" | "name"> | { id; name } |
Exclude<U, M> | 联合类型 | 不能赋值给 M 的联合成员 | Exclude<"a" | "b" | "c", "a"> | "b" | "c" |
在 Omit 和 Pick 之间如何选择,取决于源类型增加属性时你希望发生什么。Omit 会自动包含新属性,适合“除了机密字段之外的全部”。Pick 永远不会包含新属性,适合“恰好这几个字段”,对任何要离开服务器的数据来说也更安全。Pick 会根据 keyof T 检查键,Omit 则不会。
常见问题
TypeScript 中的 Omit 有什么作用?
Omit<T, K> 会构建一个新的对象类型,包含 T 的所有属性,但去掉键在 K 中的那些。Omit<User, "password"> 就是不带 password 的 User。它只改变类型,运行时不会从对象中删除任何东西。
TypeScript 中如何用 Omit 移除多个属性?
把多个键组成的联合类型作为第二个参数传入:Omit<User, "password" | "createdAt" | "updatedAt">。联合类型中的每个键都会被移除。
Omit 和 Exclude 有什么区别?
Omit 从对象类型中移除属性:Omit<{ a: 1; b: 2 }, "a"> 得到 { b: 2 }。Exclude 从联合类型中移除成员:Exclude<"a" | "b", "a"> 得到 "b"。Omit 本身就是用 Exclude 实现的:Pick<T, Exclude<keyof T, K>>。
为什么 Omit 遇到不存在的键不报错?
它的键参数约束为 keyof any(任意字符串、数字或 symbol),而不是 keyof T,所以像 Omit<User, "pasword"> 这样的拼写错误照样能编译,只是什么也没移除。定义 type StrictOmit<T, K extends keyof T> = Omit<T, K>,未知的键就会报错。
TypeScript 中如何在运行时删除对象的属性?
用带剩余元素的解构:const { password, ...rest } = user;。TypeScript 会把 rest 的类型推断为不含 password 的对象,结构与 Omit<User, "password"> 相同,而且新对象里确实没有这个属性了。