Step by step tutorial on how to create Typescript deep merge generic type which works with inconsistent key values structures.
Source code for DeepMergeTwoTypes generic is at bottom of the article. You can copy-paste it into your IDE and play with it.
you can play with the code here
Or check the GitHub repo 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>
If you want to deep dive into advanced typescript types I recommend this typescript series full of useful examples.
-
Basic static types inferring: https://dev.to/svehla/typescript-inferring-stop-writing-tests-avoid-runtime-errors-pt1-33h7
-
More advanced generics https://dev.to/svehla/typescript-generics-stop-writing-tests-avoid-runtime-errors-pt2-2k62
First of all, we’ll look at the problem with the Typescript type merging. Let’s define two types A
and B
and a new type MergedAB
which is the result of the merge A & B
.
type A = { key1: string, key2: string }
type B = { key1: string, key3: string }
type MergedAB = (A & B)['key1']
Everything looks good until you start to merge inconsistent data types.
type A = { key1: string, key2: string }
type B = { key2: null, key3: string }
type MergedAB = (A & B)
As you can see type A
define key2
as a string but type B
define key2
as a null
value.
Typescript resolves this inconsistent type merging as type never
and type MergedAB
stops to work at all. Our expected output should be something like this
type ExpectedType = {
key1: string | null,
key2: string,
key3: string
}
Let’s created a proper generic that recursively deep merge Typescript types.
First of all, we define 2 helper generic types.
type GetObjDifferentKeys<
T,
U,
T0 = Omit<T, keyof U> & Omit<U, keyof T>,
T1 = {
[K in keyof T0]: T0[K]
}
> = T1
this type takes 2 Objects and returns a new object contains only unique keys in A
and B
.
type A = { key1: string, key2: string }
type B = { key2: null, key3: string }
type DifferentKeysAB = (GetObjDifferentKeys<A, B>)['k']
For the opposite of the previous generic, we will define a new one that picks all keys which are the same in both objects.
type GetObjSameKeys<T, U> = Omit<T | U, keyof GetObjDifferentKeys<T, U>>
The returned type is an object.
type A = { key1: string, key2: string }
type B = { key2: null, key3: string }
type SameKeys = GetObjSameKeys<A, B>
All helpers functions are Done so we can start to implement the main DeepMergeTwoTypes
generic.
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
This generic finds all nonshared keys between object T
and U
and makes them optional thanks to Partial<>
generic provided by Typescript. This type with Optional keys is merged via &
an operator with the object that contains all T
and U
shared keys which values are of type T[K] | U[K]
.
As you can see in the example below. New generic found non-shared keys and make them optional ?
the rest of keys is strictly required.
type A = { key1: string, key2: string }
type B = { key2: null, key3: string }
type MergedAB = DeepMergeTwoTypes<A, B>
But our current DeepMergeTwoTypes
generic does not work recursively to the nested structures types. So let’s extract Object merging functionality into a new generic called MergeTwoObjects
and let DeepMergeTwoTypes
call recursively until it merges all nested structures.
// 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
PRO TIP: You can see that in the DeepMergeTwoTypes an if-else condition we merged type T
and U
into tuple [T, U]
for verifying that both types passed successfully the condition (similarly as the &&
operator in the javascript conditions)
This generic checks that both parameters are of type { [key: string]: unknown }
(aka Object
). If it’s true it merges them via MergeTwoObject<>
. This process is recursively repeated for all nested objects.
And voilá 🎉 now the generic is recursively applied on all nested objects example:
type A = { key: { a: null, c: string} }
type B = { key: { a: string, b: string} }
type MergedAB = DeepMergeTwoTypes<A, B>
Is that all?
Unfortunately not… Our new generic does not support Arrays.
Before we will continue we have to know the keyword infer
.
infer
look for data structure and extract data type which is wrapped inside of them (in our case it extract data type of array) You can read more about infer
functionality there:
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types
Head
This generic takes an array and returns the first item.
type Head<T> = T extends [infer I, ...infer _Rest] ? I : never
type T0 = Head<['x', 'y', 'z']>
This generic takes an array and returns all items exclude the first one.
type Tail<T> = T extends [infer _I, ...infer Rest] ? Rest : never
type T0 = Tail<['x', 'y', 'z']>
That is all we need for the final implementation of arrays merging Generic, so let's hack it!
Zip_DeepMergeTwoTypes
is a simple recursive generic which zip two arrays into one by combining their items based on the item index position.
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' }
]
>
Now we'll just write 2 lines long integration in the DeepMergeTwoTypes<T, U>
Generic which provides zipping values thanks to Zip_DeepMergeTwoTypes
Generic.
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
We did it! Values are correctly merged even for nullable values, nested objects, and long arrays.
Let’s try it on some more complex data
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
you can play with the code here
Or check the GitHub repo https://github.com/Svehla/TS_DeepMerge
If you're interested in another advanced usage of the Typescript type system, you can check these step-by-step articles/tutorials on how to create some advanced Typescript generics.
- World-first Static time RegEx engine with O(0) time complexity
- How to Object.fromEntries tuples
- UPPER_CASE to lowerCase transformator
- and so on
🎉🎉🎉🎉🎉