我有一个案例,我想“合并”类型,其中默认的类型连接(即T | U
或T & U
)没有达到我想要的。
我尝试做的是一个深度和智能的类型合并,它将在合并过程中自动将属性标记为可选,并执行TypeScript接口/类型的深度合并。
给予个例子,假设我们有类型A
和B
。
type A = {
a: string;
b: number;
c: boolean;
d: {
a2: string;
b2: number;
};
e?: number;
};
type B = {
a: string;
b: boolean;
d: {
a2: string;
c2: boolean;
};
};
我正在寻找一个Merge
函数,它将接受2个泛型类型
type Merge<T, U> = ?????;
然后,如果在类型A
和B
上使用,则输出如下所示
type AB = {
a: string;
b: number | boolean;
c?: boolean;
d: {
a2: string;
b2?: number;
c2?: boolean;
};
e?: number;
};
如图所示,Merge
类型将执行以下逻辑:
1.如果T
和U
上都存在该属性,并且是相同的类型,请将其标记为必需,并在T
/U
中设置为该类型(与a
属性类似)。
1.如果属性在T
和U
上都存在,但类型不同,则将其标记为required,如果它是基元(类似于属性b
),则将其设置为union类型,如果它是对象(类似于属性d
),则执行递归合并。
1.如果属性存在于一个类型上,而不存在于另一个类型上,则将属性标记为可选,并将其设置为它实际存在的输入类型中的类型(类似于属性c
以及b2
和c2
的情况)。
1.如果属性在一个类型中已经是可选的,那么它在输出类型中也应该是可选的,并且应用上面的现有规则来确定它的值(就像e
属性所发生的那样)
假设您可以使用递归条件类型,尽管我认识到它们是aren't yet officially supported,不应该在生产中使用,我可以为生产用例制作一个类似于jcalz@的解决方案here的展开版本。
这是为测试这道题而设置的操场。
2条答案
按热度按时间w8f9ii691#
**TLDR:**魔法!试试Playground
所以,这是一个棘手的问题。与其说是因为合并要求,不如说是因为边缘情况。获得低挂水果花了〈20分钟。确保它在任何地方都工作又花了几个小时...并且长度增加了三倍。工会是棘手的!
1.什么是可选属性?在
{ a: 1 | undefined, b?: 1 }
中a
是可选属性吗?有些人说是,有些人说不是,我个人只把b
放在可选列表中。1.你如何处理联合体?
Merge<{}, { a: 1} | { b: 2 }>
的输出是什么?我认为最有意义的类型是{ a?: 1 } | { b?: 2 }
。Merge<string, { a: 1 }>
呢?如果你根本不关心联合体,这很容易......如果你关心,那么你必须考虑所有这些。(我在括号中选择的)Merge<never, never>
(never
)Merge<never, { a: 1 }>
({ a?: 1 }
)Merge<string, { a: 1 }>
(x1米11英寸1x)Merge<string | { a: 1 }, { a: 2 }>
(string | { a: 1 | 2 }
)让我们从帮助器开始,来弄清楚这种类型。
当我想到联合体的时候,我就有了一个暗示,这个类型会变得复杂。TypeScript没有一个很好的内置方法来测试类型是否相等,但是我们可以编写一个帮助器类型,如果两个类型不相等,它会导致编译器错误。
(Note:
Test
类型可以改进,它可以允许不等价的类型传递,但它对于我们在这里的使用来说已经足够了,同时保持相当简单)我们可以像这样使用这个辅助对象:
接下来,我们需要两个helper类型。一个用于获取对象的所有必需键,另一个用于获取可选键。首先,一些测试来描述我们的目标:
这里有两件事需要注意。首先,
*Keys<never>
是never
。这一点很重要,因为我们稍后将在联合体中使用这些助手,如果对象是never
,它不应该贡献任何键。其次,这些测试都不包括联合检查。考虑到我所说的联合体的重要性,这可能会让你感到惊讶。然而,这些类型仅在所有联合体分发后使用,因此它们的行为无关紧要(尽管如果您在项目中包含这些类型,您可能希望查看所述行为,由于其编写方式,它可能与您对RequiredKeys
的预期不同)这些类型通过给定的检查:
关于这些的几个注意事项:
1.使用
-?
删除属性的可选性,这样可以避免使用Exclude<..., undefined>
的 Package 器T extends Record<K, T[K]>
可以工作,因为{ a?: 1 }
* 不 * 扩展{ a: 1 | undefined }
。在最终解决这个问题之前,我经历了几次迭代。您还可以使用另一个Map类型(如jcalz does here)检测可选性。1.在版本3.8.3中,TypeScript可以正确推断
OptionalKeys
的返回类型可分配给keyof T
。但是,它无法检测RequiredKeys
的相同类型。与keyof T
交叉修复了此问题。现在我们有了这些帮助器,我们可以定义另外两个表示业务逻辑的类型,我们需要
RequiredMergeKeys<T, U>
和OptionalMergeKeys<T, U>
。和一些测试,以确保这些行为如预期:
现在我们有了这些,我们可以定义两个对象的合并,暂时忽略原语和联合,这将调用我们还没有定义的顶级
Merge
类型来处理成员的原语和联合。(我没有在这里编写特定的测试,因为我在下一个级别有它们)
我们需要处理联合体和非对象。接下来让我们处理对象的联合体。根据前面的讨论,我们需要分布在所有类型上并单独合并它们。这非常简单。
注意我们对
[T] extends [never]
和[U] extends [never]
有额外的检查,这是因为在一个分配子句中的never
就像for (let i = 0; i < 0; i++)
,它永远不会进入条件的“body”,因此会返回never
,但是我们只需要never
,如果 * 两个 * 类型都是never
。我们快到了!现在我们可以处理合并对象了,这是这个问题最难的部分。剩下的就是处理原语,我们可以通过形成所有可能原语的并集,并排除传递给
MergeObjects
的类型的原语来完成。有了这个类型,我们就大功告成了!
Merge
的行为符合预期,只有大约50行未注解的疯狂代码。@petroni在评论中提到,这种类型不能很好地处理同时存在于两个对象中的数组。有几种不同的方法可以处理这种情况,特别是因为TypeScript的元组类型已经变得越来越灵活。正确地合并
[1, 2]
和[3]
可能会给予[1 | 3, 2?]
...但这样做至少和我们的一个简单得多的解决方案是完全忽略元组,并且总是生成一个数组,所以这个例子将生成(1 | 2 | 3)[]
。关于生成类型的最后一点说明:
Merge
的结果类型现在是正确的,但是它不是那么可读。现在将鼠标悬停在结果类型上将显示一个交集和内部对象,而不是显示结果。我们可以通过引入Expand
类型来修复这个问题,该类型强制TS将所有内容扩展为单个对象。现在只需修改
MergeNonUnionObjects
以调用Expand
。在必要的情况下,这是一种尝试和错误。您可以尝试包含它或不包含它,以获得适合您的类型显示。在操场上看看吧,里面包括了我用来验证结果的所有测试。
6mw9ycah2#
我尝试使用type-fest库中的MergeDeep类型和ramda的mergeDeepWith方法来解决这个问题。
demo code