How to deep merge in Typescript

svehla

Jakub Švehla

Posted on November 7, 2020

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.

🎉🎉🎉🎉🎉

💖 💪 🙅 🚩
svehla
Jakub Švehla

Posted on November 7, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

How to deep merge in Typescript
typescript How to deep merge in Typescript

November 7, 2020