发布于 2026-01-06 6 阅读
0

如何在 TypeScript 中进行深度合并

如何在 TypeScript 中进行深度合并

一步一步教你如何创建 TypeScript 深度合并泛型类型,使其能够处理不一致的键值结构。

总结:

DeepMergeTwoTypes 泛型的源代码位于文章底部。
您可以将其复制粘贴到您的 IDE 中进行尝试。

你可以在这里尝试修改代码。

或者查看 GitHub 代码库:https://github.com/Svehla/TS_DeepMerge



type A = { key1: { a: { b: 'c' } }, key2: undefined }
type B = { key1: { a: {} }, key3: string }
type MergedAB = DeepMergeTwoTypes<A, B>


Enter fullscreen mode Exit fullscreen mode

替代文字

先决条件

如果你想深入了解高级 TypeScript 类型,我推荐这个 TypeScript 系列教程,里面有很多有用的示例。

TypeScript&运算符行为问题

首先,我们来看看 TypeScript 类型合并的问题。我们定义两个类型AB一个新类型,新类型MergedAB是合并的结果A & B



type A = { key1: string, key2: string }
type B = { key1: string, key3: string }

type MergedAB = (A & B)['key1']


Enter fullscreen mode Exit fullscreen mode

替代文字

一切看起来都很顺利,直到你开始合并不一致的数据类型。



type A = { key1: string, key2: string }
type B = { key2: null, key3: string }

type MergedAB = (A & B)


Enter fullscreen mode Exit fullscreen mode

如您所见,类型A被定义key2为字符串,但类型也B被定义key2null值。

替代文字

TypeScript 会将这种不一致的类型合并解析为类型,导致 `type`never和 `type`MergedAB完全失效。我们期望的输出应该类似于这样:



type ExpectedType = {
  key1: string | null,
  key2: string,
  key3: string
}


Enter fullscreen mode Exit fullscreen mode

逐步解答

让我们创建一个合适的泛型,它可以递归地深度合并 TypeScript 类型。

首先,我们定义 2 个辅助泛型类型。

GetObjDifferentKeys<>



type GetObjDifferentKeys<
  T,
  U,
  T0 = Omit<T, keyof U> & Omit<U, keyof T>,
  T1 = {
    [K in keyof T0]: T0[K]
  }
 > = T1


Enter fullscreen mode Exit fullscreen mode

此类型接受 2 个对象,并返回一个新对象,该对象仅包含A和 中的唯一键B



type A = { key1: string, key2: string }
type B = { key2: null, key3: string }

type DifferentKeysAB = (GetObjDifferentKeys<A, B>)['k']


Enter fullscreen mode Exit fullscreen mode

替代文字

GetObjSameKeys<>

与前面的通用方法相反,我们将定义一个新的通用方法,该方法选取两个对象中相同的所有键。



type GetObjSameKeys<T, U> = Omit<T | U, keyof GetObjDifferentKeys<T, U>>


Enter fullscreen mode Exit fullscreen mode

返回的类型为对象。



type A = { key1: string, key2: string }
type B = { key2: null, key3: string }
type SameKeys = GetObjSameKeys<A, B>


Enter fullscreen mode Exit fullscreen mode

替代文字

所有辅助函数都已完成,所以我们可以开始实现主要的DeepMergeTwoTypes通用功能了。

DeepMergeTwoTypes<>



type DeepMergeTwoTypes<
  T,
  U, 
  // non shared keys are optional
  T0 = Partial<GetObjDifferentKeys<T, U>>
    // shared keys are required
    & { [K in keyof GetObjSameKeys<T, U>]: T[K] | U[K] },
  T1 = { [K in keyof T0]: T0[K] }
> = T1



Enter fullscreen mode Exit fullscreen mode

此泛型查找对象之间所有非共享键T,并借助 TypeScript 提供的泛型U将其设为可选键。此带有可选键的类型通过运算符与包含所有非共享键其值类型为.的对象Partial<>合并。&TUT[K] | U[K]

如下例所示,新的通用方法找到了非共享键,并将其设为可选,?其余键则为必需键。



type A = { key1: string, key2: string }
type B = { key2: null, key3: string }
type MergedAB = DeepMergeTwoTypes<A, B>


Enter fullscreen mode Exit fullscreen mode

替代文字

但是我们当前的DeepMergeTwoTypes泛型函数无法递归地处理嵌套结构类型。因此,让我们将对象合并功能提取到一个名为 `Memory` 的新泛型函数中MergeTwoObjects,并让它DeepMergeTwoTypes递归调用该函数,直到合并所有嵌套结构。



// this generic call recursively DeepMergeTwoTypes<>

type MergeTwoObjects<
  T,
  U, 
  // non shared keys are optional
  T0 = Partial<GetObjDifferentKeys<T, U>>
  // shared keys are recursively resolved by `DeepMergeTwoTypes<...>`
  & {[K in keyof GetObjSameKeys<T, U>]: DeepMergeTwoTypes<T[K], U[K]>},
  T1 = { [K in keyof T0]: T0[K] }
> = T1

export type DeepMergeTwoTypes<T, U> =
  // check if generic types are arrays and unwrap it and do the recursion
  [T, U] extends [{ [key: string]: unknown}, { [key: string]: unknown } ]
    ? MergeTwoObjects<T, U>
    : T | U


Enter fullscreen mode Exit fullscreen mode

专业提示:您可以看到,在 DeepMergeTwoTypes 的 if-else 条件中,我们将类型合并到一个元组中T以验证两种类型是否都成功通过了条件判断(类似于JavaScript 条件中的运算符)。U[T, U]&&

此泛型检查两个参数是否均为类型{ [key: string]: unknown }(即Object)。如果为真,则通过合并操作将它们合并MergeTwoObject<>。此过程会递归地对所有嵌套对象重复执行。

瞧!🎉 现在泛型可以递归地应用于所有嵌套对象,
例如:



type A = { key: { a: null, c: string} }
type B = { key: { a: string, b: string} }

type MergedAB = DeepMergeTwoTypes<A, B>


Enter fullscreen mode Exit fullscreen mode

替代文字

就这些吗?

很遗憾,我们新的泛型不支持数组。

添加数组支持

在继续之前,我们必须知道关键词infer

infer查找数据结构并提取其内部的数据类型(在本例中,提取的是数组的数据类型)。您可以infer在此处阅读更多关于该功能的信息:
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-in ​​ference-in-conditional-types

让我们定义另一个辅助泛型!

Head<T>

Head此泛型函数接受一个数组并返回第一个元素。



type Head<T> = T extends [infer I, ...infer _Rest] ? I : never

type T0 = Head<['x', 'y', 'z']>


Enter fullscreen mode Exit fullscreen mode

替代文字

Tail<T>

通用函数接受一个数组,并返回除第一个元素之外的所有元素。



type Tail<T> = T extends [infer _I, ...infer Rest] ? Rest : never

type T0 = Tail<['x', 'y', 'z']>


Enter fullscreen mode Exit fullscreen mode

替代文字

这就是实现数组合并泛型最终功能所需的全部内容,让我们开始吧!

Zip_DeepMergeTwoTypes<T, U>

Zip_DeepMergeTwoTypes是一个简单的递归泛型函数,它根据项目索引位置将两个数组合并为一个数组。



type Head<T> = T extends [infer I, ...infer _Rest] ? I : never
type Tail<T> = T extends [infer _I, ...infer Rest] ? Rest : never

type Zip_DeepMergeTwoTypes<T, U> = T extends []
  ? U
  : U extends []
  ? T
  : [
      DeepMergeTwoTypes<Head<T>, Head<U>>,
      ...Zip_DeepMergeTwoTypes<Tail<T>, Tail<U>>
  ]

type T0 = Zip_DeepMergeTwoTypes<
  [
    { a: 'a', b: 'b'},
  ],
  [
    { a: 'aaaa', b: 'a', c: 'b'},
    { d: 'd', e: 'e', f: 'f' }
  ]
>



Enter fullscreen mode Exit fullscreen mode

替代文字

现在我们只需在DeepMergeTwoTypes<T, U>通用模块中编写两行集成代码,该模块由于Zip_DeepMergeTwoTypes通用模块而提供压缩值。



export type DeepMergeTwoTypes<T, U> =
  // ----- 2 added lines ------
  // this line ⏬
  [T, U] extends [any[], any[]]
    // ... and this line ⏬
    ? Zip_DeepMergeTwoTypes<T, U>
    // check if generic types are objects
    : [T, U] extends [{ [key: string]: unknown}, { [key: string]: unknown } ]
      ? MergeTwoObjects<T, U>
      : T | U


Enter fullscreen mode Exit fullscreen mode

好了……就这些啦!!!🎉

我们成功了!即使是可为空的值、嵌套对象和长数组,值也能正确合并。

让我们用一些更复杂的数据来试试。



type A = { key1: { a: { b: 'c'} }, key2: undefined }
type B = { key1: { a: {} }, key3: string }


type MergedAB = DeepMergeTwoTypes<A, B>


Enter fullscreen mode Exit fullscreen mode

替代文字

完整源代码



type Head<T> = T extends [infer I, ...infer _Rest] ? I : never
type Tail<T> = T extends [infer _I, ...infer Rest] ? Rest : never

type Zip_DeepMergeTwoTypes<T, U> = T extends []
  ? U
  : U extends []
  ? T
  : [
      DeepMergeTwoTypes<Head<T>, Head<U>>,
      ...Zip_DeepMergeTwoTypes<Tail<T>, Tail<U>>
  ]


/**
 * Take two objects T and U and create the new one with uniq keys for T a U objectI
 * helper generic for `DeepMergeTwoTypes`
 */
type GetObjDifferentKeys<
  T,
  U,
  T0 = Omit<T, keyof U> & Omit<U, keyof T>,
  T1 = { [K in keyof T0]: T0[K] }
 > = T1
/**
 * Take two objects T and U and create the new one with the same objects keys
 * helper generic for `DeepMergeTwoTypes`
 */
type GetObjSameKeys<T, U> = Omit<T | U, keyof GetObjDifferentKeys<T, U>>

type MergeTwoObjects<
  T,
  U, 
  // non shared keys are optional
  T0 = Partial<GetObjDifferentKeys<T, U>>
  // shared keys are recursively resolved by `DeepMergeTwoTypes<...>`
  & {[K in keyof GetObjSameKeys<T, U>]: DeepMergeTwoTypes<T[K], U[K]>},
  T1 = { [K in keyof T0]: T0[K] }
> = T1

// it merge 2 static types and try to avoid of unnecessary options (`'`)
export type DeepMergeTwoTypes<T, U> =
  // ----- 2 added lines ------
  [T, U] extends [any[], any[]]
    ? Zip_DeepMergeTwoTypes<T, U>
    // check if generic types are objects
    : [T, U] extends [{ [key: string]: unknown}, { [key: string]: unknown } ]
      ? MergeTwoObjects<T, U>
      : T | U



Enter fullscreen mode Exit fullscreen mode

你可以在这里尝试修改代码。

或者查看 GitHub 代码库:https://github.com/Svehla/TS_DeepMerge

接下来呢?

如果您对 Typescript 类型系统的其他高级用法感兴趣,可以查看这些关于如何创建一些高级 Typescript 泛型的分步文章/教程。

🎉🎉🎉🎉🎉

文章来源:https://dev.to/svehla/typescript-how-to-deep-merge-170c