Step by step tutorial on how to create Typescript deep merge generic type which works with inconsistent key values structures.
TLDR:
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>
Prerequisite
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
Typescript &
operator behavior problem
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
}
Step-by-step Solution
Let’s created a proper generic that recursively deep merge Typescript types.
First of all, we define 2 helper generic types.
GetObjDifferentKeys<>
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']
GetObjSameKeys<>
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.
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
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.
Add arrays support
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
Let's define another helper generics!
Head<T>
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']>
Tail<T>
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<T, U>
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
And…. That’s all!!! 🎉
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>
Full source code
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
And what's next?
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
🎉🎉🎉🎉🎉
Top comments (10)
Hi. First of all, thanks so much for the article! It saved me.
Secondly, I would like to share a problem i faced using your code and how i managed to solve it.
While trying to merge array of objects into one object using your MergeTwoObjects
I faced with the errror: 'Type parameter 'K' has a circular constraint.
To solve it i moved 'T0' from generic parameters into separate type
Hope it gonna help smb in such wide-spread case
Great article. Thank you for it! The more TS recepies we have the better types we can write. Especially thanks for this hack with a tuple and if-else.
One note: There's type PropertyKey. It'd be better to use it instead of
string
. But... TS dissalow it. It even dissallows usingstring | number
. Hm...P.S. a translated this article to Russian there ( habr.com/en/post/526998/ ).
Hi dude
Thanks a lot for the translation 😇 a read the comments below and that pretty nice 💪
some guy found issue with merging inconsistent arrays
[{ a: 'a' }, { b: 'b'}]
... I know about it and I already resolved it... but the solution is too complicated (and large) to keep it in one article... So I have a plan to do the second part of it where I'll resolve this edge-case + Add the optional length of types to merge... something like:DeepMergeMany<A, B, C, D, ...>
So I'll see 😏
Hi @stepan Zubaslev! It took few months but I did it! :D
I just refactored half of the article and I add
At the moment the basic type like
{ a: [{ a: 'a' }, { b: 'b'}] }
is working andDeepMergeTwoTypes<T, U>
generic will resolve and merge this array (tuple) structure correctly, like in the screenshots in the article.I hope you'll like these newly refactored upgrades!
Thanks for writing the article.
I tried to workaround this question with the MergeDeep type from the type-fest library and ramda's mergeDeepWith method.
demo code
Extremely useful.
haha 😇 thanks a lot!
Love your creative solution.
This also shows how TS is unable to deal with basic, and also very very typical programming patterns of JS. It is just not mature enough (yet), to replace JS proper.
thank you a lot :) I'm happy that you enjoy the article
Looks great. Unfortunately when I use it with two types, when building the app it says
Type error: Type 'DeepMergeTwoTypes' is not generic.
:-(