/TS_DeepMerge

Primary LanguageTypeScript

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>

deep merge preview

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

Ts native merge

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.

Ts native merge 2

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

different keys

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>

Same keys

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>

deep merge two types

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>

deep merge two types 2

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

Head array

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

tail array

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

zip deep merge two types

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>

deep merge preview

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.

🎉🎉🎉🎉🎉