你的 Omit 类型还可以更严格一些

简介: > 本文是对在极客时间 与 [早早聊](https://www.zaozao.run/conf/c37) 的直播 中提到的 **Omit 工具类型** 的进一步说明,但你不需要已经观看过相关直播,本文会包括前置知识部分。## Pick 与 OmitPick 与 Omit 都是 TypeScript 内置的工具类型,它们的作用类似,都是对接口做剪裁,如```typescriptinte
本文是对在极客时间 与 早早聊 的直播 中提到的 Omit 工具类型 的进一步说明,但你不需要已经观看过相关直播,本文会包括前置知识部分。

Pick 与 Omit

Pick 与 Omit 都是 TypeScript 内置的工具类型,它们的作用类似,都是对接口做剪裁,如

interface Foo {
    a: number;
    b: string;
    c: boolean;
}

// { a:number; }
type OnlyA = Pick<Foo, "a">;

// { b: string; c: boolean}
type ExcludeA = Omit<Foo, "a">;

它们俩的功能是相反的,这其实代表了 TypeScript 类型编程中的一个概念:基本上所有的工具类型都有其反向实现,其产生方式通常有两种,对一个工具类型进行简单的条件更改就能得到另一个功能相反的工具类型(如 Partial 与 Required),或反向类型基于正向类型实现(即 Pick 与 Omit)。

我们直接看 Pick 与 Omit 的实现:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Pick 就是简单的使用了索引类型(索引签名与索引类型访问)与映射类型、keyof操作符,这里不做展开介绍。

keyof 操作符常和接口结构一起使用,得到一组对象键值的字面量类型组成的联合类型,如 'a'|'b'|'c'。我们也常用 keyof any 表示成员未知的联合类型。

Omit 的实现有趣一些,它基于 Pick 类型实现,相反的,其第二个泛型参数 K 在传入 Pick 类型时进行了一次额外的转换,Exclude<keyof T, K>可能有点绕,但实际上:

type Exclude<T, U> = T extends U ? never : T;

Exclude<Set1, Set2> 的作用就是得到 Set1 相对于 Set2 的差集,即:

  • Exclude<'a'|'b'|'c', 'a'|'d'|'e'> 的结果为 'b'|'c',即 Set1 中有,而 Set2 中没有的部分。
关于通过分布式条件类型实现差集、补集、并集、交集及将其应用扩展到二维对象类型,见此账号同期发布的 「分布式条件类型全知」。

再回到 Pick 和 Omit 的场景,Exclude 的结果即为 'b'|'c',相当于与 Pick 的思路反向而行,这样我们就得到了 Omit。

再看上面的 Omit 类型,你会发现一个很奇怪的地方:明明它们的作用都是裁剪对象,那么第二个泛型参数 K 应当被约束为 keyof T 才对,为什么只有 Pick 被约束了?

为什么 Omit 放水了?

你可以在 #30825 中阅读更多讨论,以下部分只摘取其中的部分探讨与笔者自己的见解。

Omit 工具类型在 TypeScript 3.5版本 中被引入,就是我们现在看到的宽松版本。而实际上,最初的 PR #30455 中,Omit 的实现的确是严格的:

type Omit<ObjectType, KeysType extends keyof ObjectType> = Pick<ObjectType, Exclude<keyof ObjectType, KeysType>>;

Daniel Rosenwasser (TypeScript 的 PM,也是现在每一期 DevBlog 的写作者) 最终引入的实现却移除了严格约束,见 #30552

关于做出决定的原因,Daniel 解释到主要是因为当时已有的 Exclude 工具类型,同样没有限制第二个参数需要为第一个参数的子集:

type Exclude<T, U> = T extends U ? never : T;

因此团队成员认为对于 Omit 类型也不应该做此类的限制。

先插一下我个人的见解,我认为这两种情况其实并不一致,对于 Exclude ,即我们认为差集,在数学层面上差集并不要求此限制,即:

如果我们要求参数2为参数1的子集,则其应该被称为补集(Complement):

export type Complement<A, B extends A> = Exclude<A, B>;

而 Omit 类型其实更贴近补集的情况,我们能够要被移除的部分就是原对象的子集,那么其 OmitKeys 也应该被约束才对。

在 #30825 中,npm之神 Sindre Sorhus 也加入了讨论(为什么说之神呢,因为你用的 npm 包大概率底层直接或间接依赖了他的开源包),他指出在许多 TypeScript 类型工具库中,基本不会直接使用内置的 Omit 类型,而是自己实现一个严格版本。这些工具库包括 type-zoo、type-fest(目前最流行的类型库,也是 Sindre Sorhus 的作品)、utility-types 等。

而 TS 团队成员认为,如果将 Omit 更改为严格版本,会导致很多 DefinitelyTyped (@types/xxx 这种)的包出现问题,因此,既然始终不能让所有人都满意,还是保持原有实现为好。

而另一位团队核心成员 Ryan Cavanaugh 也指出,并不是在所有情况下此约束都会带来更好的效果,比如我们需要使用 keyof Obj2 来剔除 Obj1:

type Omit1<T, K> = Pick<T, Exclude<keyof T, K>>;
type Omit2<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

// 这里就不能用严格 Omit 了
declare function combineSpread<T1, T2>(obj: T1, otherObj: T2, rest: Omit1<T1, keyof T2>): void;

type Point3d = { x: number, y: number, z: number };

declare const p1: Point3d;

// 能够检测出错误,rest 中缺少了 y
combineSpread(p1, { x: 10 }, { z: 2 });
认真地说,我认为这其实不是 Omit 类型应该做的事,叫它 Remove 可能更合适...

后面还有很多很多脑洞大开的讨论,比如,通过 lib:['omit.loose'] / lib:['omit.strict'] 来显式控制行为,通过 TypeScript ESLint 的 Ban Types 规则禁用掉对 Omit 类型的使用等等。

当然,既然我们今天看到的 Omit 类型还是宽松版本,就说明最后社区还是没有说服团队成员。Ryan 在最后的 Close Comment 中总结了几点原因:

  • 并不是所有人都希望内置严格的 Omit 类型,支持者最多只有 70%。
  • StrictOmit 是具有传染性的,可能导致一批下游依赖的类型声明出问题,让开发者选择是要宽松还是阉严格更符合直觉。
  • 就算 TS 直接用掉了 Omit 这个名字,其实社区还可以用 StrictOmit、Except(type-fest中)、Remove 等名字(Sindre 批评说占用了这个名字却只实现了阉割功能)

扩展

以下扩展和本文主旨无关,属于对 Pick 和 Omit 类型的扩展,欢迎你将它们作为习题进一步独立研究。

  • Pick 和 Omit 是通过键来裁剪的,请实现基于值裁剪的 PickByValue 与 OmitByValue 类型。
  • 在上一题的基础上,实现严格的基于值的裁剪,如对于联合类型的键值类型,需要其完全的匹配(如 PickByValue<T, 'a'|'b'|'c'> 不能保留类型为 'a' 的键)。
相关文章
|
XML JSON API
淘宝天猫API接入说明(淘宝天猫商品详情+关键词搜索商品列表)商品详情数据,商品sku数据,商品优惠券数据
业务场景:作为全球最大的 B2C 电子商务平台之一,淘宝天猫平台提供了丰富的商品资源,吸引了大量的全球买家和卖家。为了方便开发者接入淘宝天猫平台,淘宝天猫平台提供了丰富的 API 接口,其中历史价格接口是非常重要的一部分。大家有探讨稳定采集淘宝(天猫)京东阿里拼多多等平台整站实时商品详情历史价格数据接口,通过该接口开发者可以更好地了解商品的情况,商品详情数据详细信息查询,数据参数包括:商品链接,商品列表主图、价格、标题,sku,库存,销量,店铺昵称,店铺等级,商品详情SKU属性,商品视频,商品优惠券,促销信息,详情属性描述,宝贝ID,区域ID,发货地,发货至,快递费用,物流费用等页面上有的数据
|
机器学习/深度学习 编解码 搜索推荐
实测13个类Sora视频生成模型,8000多个案例,一次看个够
SORA-like模型是一类基于OpenAI的SORA模型发展而来的视频生成技术,以其在生成高质量视频上的卓越表现受到关注。该模型不仅提升了视频的分辨率、自然度和视觉语言对齐,还增强了对长视频序列的可控性。适用于内容创作、世界模拟等多种场景,展现出广泛的应用潜力。然而,模型在自动化评估、与人类偏好匹配及处理复杂运动上仍面临挑战。未来研究将聚焦于多模态、连续、交互式及个性化视频生成等领域。
888 2
|
存储 数据采集 监控
开源日志Fluentd
【10月更文挑战第21天】
301 7
|
SQL 关系型数据库 MySQL
MySQL数据库——触发器-介绍、语法(创建,查看,删除)
MySQL数据库——触发器-介绍、语法(创建,查看,删除)
1111 0
|
Ubuntu Unix Linux
Docker 镜像(image)& 容器(container)
什么是 Docker 镜像(image)? 镜像由多个层组成,每层叠加之后,从外部看来就如一个独立的对象; 镜像内部是一个精简的操作系统(OS),同时还包含应用运行所必须的文件和依赖包; 镜像可以运行一个或多个容器,同时镜像也可以停止某个容器的运行,并从中创建新的镜像;【镜像(iamge)的分类】...
1189 1
Docker 镜像(image)& 容器(container)
|
Web App开发 编解码 移动开发
React 框架下如何集成 H.265 流媒体视频播放器 EasyPlayer.js?
H5 无插件流媒体播放器 EasyPlayer 属于一款高效、精炼、稳定且免费的流媒体播放器,可支持多种流媒体协议播放,可支持 H.264 与 H.265 编码格式,性能稳定、播放流畅,能支持 WebSocket-FLV、HTTP-FLV,HLS(m3u8)、WebRTC 等格式的视频流。在功能上,EasyPlayer 支持直播、点播、录像、快照截图、MP4 播放、多屏播放、倍数播放、全屏播放等特性,并且已实现网页端实时录像、在 iOS 上实现低延时直播等功能,具备较高的可用性和稳定性
535 0
|
JavaScript 应用服务中间件 nginx
docker安装的nginx放在html文件下的vue项目404解决
docker安装的nginx放在html文件下的vue项目404解决
333 0
|
传感器 算法 自动驾驶
基于uwb和IMU融合的三维空间定位算法matlab仿真
基于uwb和IMU融合的三维空间定位算法matlab仿真
|
芯片 SoC
OpenHarmony 标准系统HDF框架之I2C驱动开发
OpenHarmony 标准系统HDF框架之I2C驱动开发
584 0
OpenHarmony 标准系统HDF框架之I2C驱动开发
|
存储 算法 NoSQL
说起分布式自增ID只知道UUID?SnowFlake(雪花)算法了解一下(Python3实现)
但凡说起分布式系统,我们肯定会对一些海量级的业务进行分拆,比如:用户表,订单表。因为数据量巨大一张表完全无法支撑,就会对其进行分库分表。但是一旦涉及到分库分表,就会引申出分布式系统中唯一主键ID的生成问题,当我们使用mysql的自增长主键(auto\_increment)时,充分感受到了它的好处:整个系统ID唯一,ID是数字类型,而且是趋势递增的,ID简短,查询效率快,在分布式系统中显然由于单点问题无法使用mysql自增长了,此时需要别的解决方案来支撑分布式业务。
说起分布式自增ID只知道UUID?SnowFlake(雪花)算法了解一下(Python3实现)