如何在 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>
先决条件
如果你想深入了解高级 TypeScript 类型,我推荐这个 TypeScript 系列教程,里面有很多有用的示例。
-
基本静态类型推断:https://dev.to/svehla/typescript-inferring-stop-writing-tests-avoid-runtime-errors-pt1-33h7
-
更高级的泛型https://dev.to/svehla/typescript-generics-stop-writing-tests-avoid-runtime-errors-pt2-2k62
TypeScript&运算符行为问题
首先,我们来看看 TypeScript 类型合并的问题。我们定义两个类型A和B一个新类型,新类型MergedAB是合并的结果A & B。
type A = { key1: string, key2: string }
type B = { key1: string, key3: string }
type MergedAB = (A & B)['key1']
一切看起来都很顺利,直到你开始合并不一致的数据类型。
type A = { key1: string, key2: string }
type B = { key2: null, key3: string }
type MergedAB = (A & B)
如您所见,类型A被定义key2为字符串,但类型也B被定义key2为null值。
TypeScript 会将这种不一致的类型合并解析为类型,导致 `type`never和 `type`MergedAB完全失效。我们期望的输出应该类似于这样:
type ExpectedType = {
key1: string | null,
key2: string,
key3: string
}
逐步解答
让我们创建一个合适的泛型,它可以递归地深度合并 TypeScript 类型。
首先,我们定义 2 个辅助泛型类型。
GetObjDifferentKeys<>
type GetObjDifferentKeys<
T,
U,
T0 = Omit<T, keyof U> & Omit<U, keyof T>,
T1 = {
[K in keyof T0]: T0[K]
}
> = T1
此类型接受 2 个对象,并返回一个新对象,该对象仅包含A和 中的唯一键B。
type A = { key1: string, key2: string }
type B = { key2: null, key3: string }
type DifferentKeysAB = (GetObjDifferentKeys<A, B>)['k']
GetObjSameKeys<>
与前面的通用方法相反,我们将定义一个新的通用方法,该方法选取两个对象中相同的所有键。
type GetObjSameKeys<T, U> = Omit<T | U, keyof GetObjDifferentKeys<T, U>>
返回的类型为对象。
type A = { key1: string, key2: string }
type B = { key2: null, key3: string }
type SameKeys = GetObjSameKeys<A, B>
所有辅助函数都已完成,所以我们可以开始实现主要的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
此泛型查找对象之间所有非共享键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>
但是我们当前的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
专业提示:您可以看到,在 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>
就这些吗?
很遗憾,我们新的泛型不支持数组。
添加数组支持
在继续之前,我们必须知道关键词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']>
Tail<T>
通用函数接受一个数组,并返回除第一个元素之外的所有元素。
type Tail<T> = T extends [infer _I, ...infer Rest] ? Rest : never
type T0 = Tail<['x', 'y', 'z']>
这就是实现数组合并泛型最终功能所需的全部内容,让我们开始吧!
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' }
]
>
现在我们只需在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
好了……就这些啦!!!🎉
我们成功了!即使是可为空的值、嵌套对象和长数组,值也能正确合并。
让我们用一些更复杂的数据来试试。
type A = { key1: { a: { b: 'c'} }, key2: undefined }
type B = { key1: { a: {} }, key3: string }
type MergedAB = DeepMergeTwoTypes<A, B>
完整源代码
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
或者查看 GitHub 代码库:https://github.com/Svehla/TS_DeepMerge
接下来呢?
如果您对 Typescript 类型系统的其他高级用法感兴趣,可以查看这些关于如何创建一些高级 Typescript 泛型的分步文章/教程。
🎉🎉🎉🎉🎉
文章来源:https://dev.to/svehla/typescript-how-to-deep-merge-170c










