我实际上已经创建了一个相当未优化的方法来做到这一点,我认为它一直有效,直到现在我又回到了它,它似乎不再有效(仅显示为 never
- 仍然有效出于某种原因在 TS 操场上)。
到目前为止,它看起来像这样:
//https://github.com/microsoft/TypeScript/issues/13298#issuecomment-707364842
type UnionToArray<T> = (
(
(
T extends any
? (t: T) => T
: never
) extends infer U
? (U extends any
? (u: U) => any
: never
) extends (v: infer V) => any
? V
: never
: never
) extends (_: any) => infer W
? [...UnionToArray<Exclude<T, W>>, W]
: []
);
type IntersectObjectArray<A extends any> = A extends [infer T, ...infer R] ? T & IntersectObjectArray<R> : unknown
type ExpandTopKeys<A extends any> = A extends { [key: string]: infer X } ? { [K in keyof X]: X[K] } : unknown
type Expand<A extends any> = IntersectObjectArray<UnionToArray<ExpandTopKeys<A>>>;
type MergedClasses<C extends object[]> = Expand<IntersectObjectArray<C>>;
它的作用是:
X = {
foo: {
a: "green",
},
bar: {
b: "blue",
}
}
Y = {
extra: {
c: "orange",
},
}
MergedClasses<[X, Y]>
将返回:
{
a: "green",
b: "blue",
c: "orange",
}
所以它需要对象,组合它们,然后将“顶级”键扩展为单个对象。
它目前经历的步骤是:
[X, Y]
变为 X & Y
foo
、bar
和 extra
,将其转换为联合,例如:{
a: "green",
b: "blue",
} | {
c: "orange",
}
[{ a: "green", b: "blue" }, { c: "orange" }]
有没有一种更简单的方法可以合并任意数量的对象,并扩展它们的任何键?
答案 0 :(得分:2)
TypeScript 中的 Union-to-tuple 是一种非常糟糕的脆弱类型操作,任何头脑正常的人都不应该考虑执行 (link to me doing this)。幸运的是,您不应该这样做来实现您的类型函数。
我在这里看到的基本操作是您有一些对象类型(包括元组),其属性键应该被忽略(对于元组,这意味着 "0"
和 "1"
等),并且属性值应该合并。这种合并本质上是一个 intersection,尽管像 {a: 1, b: 2, c: 3}
这样的单个合并对象可能比像 {a: 1} & {b: 2} & {c: 3}
这样的可变参数交集更可取。我们将此操作称为 MergeProperties<T>
。所以 MergeProperties<{x: {a: 1}, y: {b: 2}>
应该是 {a: 1, b: 2}
,而 MergeProperties<[{c: 3}, {d: 4}]>
应该是 {c: 3, d: 4}
。
那么,从概念上讲,您对 MergedClasses<T>
所做的是 MergeProperties<MergeProperties<T>>
;您正在合并所有元组元素,然后合并结果的属性。
以下是 MergeProperties<T>
的可能实现:
type MergeProperties<T> =
(({ [K in keyof T]-?: (x: T[K]) => void }[keyof T]) extends
((x: infer I) => void) ? I : never
) extends infer O ? { [K in keyof O]: O[K] } : never;
extends infer O ...
之前的所有内容都对 T
的所有属性执行可变参数交集。没有内置的类型函数可以做到这一点;如果您 look up T
的所有属性(如 T[keyof T]
),您会得到 union 而不是交集。幸运的是,我们可以使用 conditional type inference 将联合转换为交集。 See this question 了解更多详细信息,但粗略地说,由于函数类型在其参数类型中的 contravariance,函数联合采用的参数是这些函数的参数类型的交集。
extends infer O ...
部分采用此可变参数交集并将其转换为单个对象类型。有关详细信息,请参阅 this question。
您可以验证此版本是否适用于常规对象类型:
type Test = MergeProperties<{ x: { a: 1 }, y: { b: 2 } }>;
/* type Test = {
a: 1;
b: 2;
} */
但不适用于元组:
type Oops = MergeProperties<[{ c: 1 }, { d: 2 }]>;
/* type Oops = never */
这是因为 mapping over array types 时的设计限制或可能是 TypeScript 中的错误,因此映射或后续查找有时会对数组类型的所有属性进行操作,即使是像 push()
和 {{1 }};详情请参阅 microsoft/TypeScript#27995。
当 length
是元组时,可以重写 MergeProperties<T>
来解决这个问题,但将 T
元组转换为更易于处理的内容同样容易:{{ 1}} 会将像 T
这样的 Omit<T, keyof any[]>
变成 T
。 The Omit
utility type 用于按键丢弃属性,丢弃 [{c: 1}, {d: 2}]
我们只剩下位置索引。
因此,我们可以这样定义 {"0": {c: 1}, "1": {d: 2}}
:
keyof any[]
并验证它至少适用于您的示例:
MergedClasses
看起来不错。这里可能有很多的边缘情况。可选属性、索引签名、联合等等,很可能会做一些奇怪的事情。您的原始版本可能有类似的问题,但它们可能大不相同,因此您应该确保在您的用例中对其进行测试。