打字稿类型将嵌套字典扩展为单个字典

时间:2021-05-11 17:12:41

标签: typescript

我实际上已经创建了一个相当未优化的方法来做到这一点,我认为它一直有效,直到现在我又回到了它,它似乎不再有效(仅显示为 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
  • 展开“顶级键”,即展开 foobarextra,将其转换为联合,例如:
{
    a: "green",
    b: "blue",
} | {
    c: "orange",
}
  • 将联合转换为对象数组,即 [{ a: "green", b: "blue" }, { c: "orange" }]
  • 最后,再次将所有那些对象相交。 做完所有这些之后,结果就是我想要的。然而,这感觉真的很不坚固而且很容易被破坏(说实话,似乎已经被破坏了)。

有没有一种更简单的方法可以合并任意数量的对象,并扩展它们的任何键?

1 个答案:

答案 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[]> 变成 TThe Omit utility type 用于按键丢弃属性,丢弃 [{c: 1}, {d: 2}] 我们只剩下位置索引。


因此,我们可以这样定义 {"0": {c: 1}, "1": {d: 2}}

keyof any[]

并验证它至少适用于您的示例:

MergedClasses

看起来不错。这里可能有很多的边缘情况。可选属性、索引签名、联合等等,很可能会做一些奇怪的事情。您的原始版本可能有类似的问题,但它们可能大不相同,因此您应该确保在您的用例中对其进行测试。

Playground link to code