DEV Community

Jakub Švehla
Jakub Švehla

Posted on • Updated on

How to deep merge in Typescript

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>
Enter fullscreen mode Exit fullscreen mode

Alt Text

Prerequisite

If you want to deep dive into advanced typescript types I recommend this typescript series full of useful examples.

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']
Enter fullscreen mode Exit fullscreen mode

Alt Text

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)
Enter fullscreen mode Exit fullscreen mode

As you can see type A define key2 as a string but type B define key2 as a null value.

Alt Text

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

Alt Text

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>>
Enter fullscreen mode Exit fullscreen mode

The returned type is an object.

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

Alt Text

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

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Alt Text

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']>
Enter fullscreen mode Exit fullscreen mode

Alt Text

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']>
Enter fullscreen mode Exit fullscreen mode

Alt Text

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' }
  ]
>

Enter fullscreen mode Exit fullscreen mode

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Alt Text

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

Enter fullscreen mode Exit fullscreen mode

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.

🎉🎉🎉🎉🎉

Discussion (8)

Collapse
faiwer profile image
Stepan Zubashev • Edited on

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 using string | number. Hm...

P.S. a translated this article to Russian there ( habr.com/en/post/526998/ ).

Collapse
svehla profile image
Jakub Švehla Author • Edited on

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 😏

Collapse
svehla profile image
Jakub Švehla Author

Hi @stepan Zubaslev! It took few months but I did it! :D

I just refactored half of the article and I add

  • Support for better typescript help
  • Fix many edge cases which I mention a few months ago.

At the moment the basic type like { a: [{ a: 'a' }, { b: 'b'}] } is working and DeepMergeTwoTypes<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!

Collapse
a2br profile image
a2br

Extremely useful.

Collapse
svehla profile image
Jakub Švehla Author

haha 😇 thanks a lot!

Collapse
domiii profile image
Domi

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.

Collapse
svehla profile image
Jakub Švehla Author

thank you a lot :) I'm happy that you enjoy the article