I know this is a bit of an old issue but the easiest solution in ES2015/ES6 I could come up with was actually quite simple, using Object.assign(),

Hopefully this helps:

/**
 * Simple object check.
 * @param item
 * @returns {boolean}
 */
export function isObject(item) {
  return (item && typeof item === 'object' && !Array.isArray(item));
}

/**
 * Deep merge two objects.
 * @param target
 * @param ...sources
 */
export function mergeDeep(target, ...sources) {
  if (!sources.length) return target;
  const source = sources.shift();

  if (isObject(target) && isObject(source)) {
    for (const key in source) {
      if (isObject(source[key])) {
        if (!target[key]) Object.assign(target, { [key]: {} });
        mergeDeep(target[key], source[key]);
      } else {
        Object.assign(target, { [key]: source[key] });
      }
    }
  }

  return mergeDeep(target, ...sources);
}

Example usage:

mergeDeep(this, { a: { b: { c: 123 } } });
// or
const merged = mergeDeep({a: 1}, { b : { c: { d: { e: 12345}}}});  
console.dir(merged); // { a: 1, b: { c: { d: [Object] } } }

You'll find an immutable version of this in the answer below.

Note that this will lead to infinite recursion on circular references. There's some great answers on here on how to detect circular references if you think you'd face this issue.

Answer from Salakar on Stack Overflow
🌐
npm
npmjs.com › package › ts-deepmerge
ts-deepmerge - npm
May 2, 2025 - A TypeScript deep merge function.. Latest version: 7.0.3, last published: a year ago. Start using ts-deepmerge in your project by running `npm i ts-deepmerge`. There are 346 other projects in the npm registry using ts-deepmerge.
      » npm install ts-deepmerge
    
Published   May 02, 2025
Version   7.0.3
Author   Raice Hannay
Top answer
1 of 15
316

I know this is a bit of an old issue but the easiest solution in ES2015/ES6 I could come up with was actually quite simple, using Object.assign(),

Hopefully this helps:

/**
 * Simple object check.
 * @param item
 * @returns {boolean}
 */
export function isObject(item) {
  return (item && typeof item === 'object' && !Array.isArray(item));
}

/**
 * Deep merge two objects.
 * @param target
 * @param ...sources
 */
export function mergeDeep(target, ...sources) {
  if (!sources.length) return target;
  const source = sources.shift();

  if (isObject(target) && isObject(source)) {
    for (const key in source) {
      if (isObject(source[key])) {
        if (!target[key]) Object.assign(target, { [key]: {} });
        mergeDeep(target[key], source[key]);
      } else {
        Object.assign(target, { [key]: source[key] });
      }
    }
  }

  return mergeDeep(target, ...sources);
}

Example usage:

mergeDeep(this, { a: { b: { c: 123 } } });
// or
const merged = mergeDeep({a: 1}, { b : { c: { d: { e: 12345}}}});  
console.dir(merged); // { a: 1, b: { c: { d: [Object] } } }

You'll find an immutable version of this in the answer below.

Note that this will lead to infinite recursion on circular references. There's some great answers on here on how to detect circular references if you think you'd face this issue.

2 of 15
254

You can use Lodash merge:

var object = {
  'a': [{ 'b': 2 }, { 'd': 4 }]
};

var other = {
  'a': [{ 'c': 3 }, { 'e': 5 }]
};

console.log(_.merge(object, other));
// => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>

Discussions

Help with typing a deep merge function.
Hey all, I'm wondering if there are any smart people out there looking a little bit of a challenge. On the surface this didn't seem like it would be… More on reddit.com
🌐 r/typescript
10
7
November 9, 2020
Deep Merging Utility Types
edit: changed the word "union" to "merge" to avoid confusion Suggestion 🔍 Search Terms "deep union" "sub property merge" "deep merge" ✅ Viability C... More on github.com
🌐 github.com
15
June 18, 2021
TypeScript Type Merging - Stack Overflow
What I am trying to make is a deep and smart type merge that will automatically mark properties as optional during the merge and perform deep merges of TypeScript interfaces/types. More on stackoverflow.com
🌐 stackoverflow.com
Javascript Object Merge
This merge will have some problems. You're better off sticking to a more battle-tested merge such as lodash's merge . More on reddit.com
🌐 r/learnjavascript
28
183
February 16, 2021
🌐
DEV Community
dev.to › svehla › typescript-how-to-deep-merge-170c
How to deep merge in Typescript - DEV Community
October 5, 2021 - 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.
🌐
GeeksforGeeks
geeksforgeeks.org › typescript › how-to-deep-merge-two-objects-in-typescript
How to Deep Merge Two Objects in TypeScript ? - GeeksforGeeks
June 16, 2025 - This approach involves recursively traversing the objects and merging their properties. When encountering nested objects, the function calls itself to perform a deep merge.
🌐
GitHub
github.com › voodoocreation › ts-deepmerge
GitHub - voodoocreation/ts-deepmerge: A TypeScript deep merge function with automatically inferred types. · GitHub
A TypeScript deep merge function with automatically inferred types. - voodoocreation/ts-deepmerge
Starred by 144 users
Forked by 15 users
Languages   TypeScript 97.2% | JavaScript 2.8%
🌐
Reddit
reddit.com › r/typescript › help with typing a deep merge function.
r/typescript on Reddit: Help with typing a deep merge function.
November 9, 2020 - Edit: I read your comment about merging an array of objects. You might have to provide a load of typed overloads for the first 1-n size arrays up to a maximum where you think it is likely people will use them (see pipe from rxjs for an example) ... Maybe you can adapt the typings to a deep merge.
🌐
Sandtoken
sandtoken.com › writing › typescript-deep-merge-explained
Deep Merge in TypeScript - A Type-Safe Approach to Object Combination
October 19, 2024 - The implementation leverages TypeScript's type system to ensure type safety during the merge operation. The generic type parameters T and R allow the function to work with any object types while maintaining their type information. Unlike Object.assign() or the spread operator, this utility performs a deep copy of nested objects, preventing unwanted reference sharing between the source and target objects.
Find elsewhere
🌐
npm
npmjs.com › package › deepmerge-ts
deepmerge-ts - npm
Deeply merge 2 or more objects respecting type information.. Latest version: 7.1.5, last published: a year ago. Start using deepmerge-ts in your project by running `npm i deepmerge-ts`. There are 3790 other projects in the npm registry using deepmerge-ts.
      » npm install deepmerge-ts
    
Published   Feb 23, 2025
Version   7.1.5
Author   Rebecca Stevens
🌐
Medium
medium.com › developersglobal › mastering-typescript-deep-object-spreading-a-complete-guide-to-merging-nested-objects-aec334b39c50
Mastering TypeScript Deep Object Spreading: A Complete Guide to Merging Nested Objects | by Abhishek kumaar | DevelopersGlobal | Medium
April 4, 2025 - The best approaches for handling deep object spreading efficiently. Shallow object spreading copies only the top-level properties, and nested objects remain referenced. const obj1 = { name: "Amit", address: { city: "Delhi", country: "India" } }; const obj2 = { age: 30 }; const merged = { ...obj1, ...obj2 }; console.log(merged); // Output: { name: "Amit", address: { city: "Delhi", country: "India" }, age: 30 }
🌐
GitHub
github.com › microsoft › TypeScript › issues › 44660
Deep Merging Utility Types · Issue #44660 · microsoft/TypeScript
June 18, 2021 - Note: if you make the output type for functions (...args: A) => R (instead of merging sub-properties), it seems to handle functions at least sort of decently. export type DeepMerge<T1, T2> = ( T1 extends object ? ( T2 extends object ?
Author   SephReed
🌐
Amitd
amitd.co › code › typescript › recursively-deep-merging-objects
Recursively deep merging objects — Typescript — Amit Dhamu — Software Engineer
deepMerge( { version: 1, a: '__PROPERTY_A__', b: { a: '__PROPERTY_A__', }, c: { a: '__PROPERTY_A__', e: '__PROPERTY_E__', }, }, { nested: true, date: new Date('2021-05-05').toISOString(), a: '__PROPERTY_A__', b: { b: '__PROPERTY_B__', }, c: { b: '__PROPERTY_B__', d: '__PROPERTY_D__', f: { a: '__PROPERTY_A__', b: '__PROPERTY_B__', c: '__PROPERTY_C__', d: { a: '__PROPERTY_A__', }, }, }, } ) // Returns { nested: true, version: 1, date: '2021-05-05T00:00:00.000Z', a: '__PROPERTY_A__', b: { a: '__PROPERTY_A__', b: '__PROPERTY_B__', }, c: { a: '__PROPERTY_A__', b: '__PROPERTY_B__', d: '__PROPERTY_D__', e: '__PROPERTY_E__', f: { a: '__PROPERTY_A__', b: '__PROPERTY_B__', c: '__PROPERTY_C__', d: { a: '__PROPERTY_A__', }, }, }, }
🌐
Aaron Bos
aaronbos.dev › posts › merge-objects-typescript-javascript
Merging Objects in TypeScript (and JavaScript)
June 11, 2022 - Any properties deeper than the first level will simply be copied as part of their parent. ... Let's take a look at Object.assign in the context of our HttpRequest interface. In this example, we create the req variable with some base values defined for the properties. We then use Object.assign to merge another object with the target.
🌐
npm
npmjs.com › package › object-deep-merge
object-deep-merge - npm
October 20, 2025 - type Data = { name: string; description: string; }; const base: Data = { name: "object-deep-merge", description: "merge objects" }; const overrides: Partial<Data> = { description: "merge objects, deeply" }; const merged = merge(base, overrides); // Type is inferred so the signature becomes: // function merge<Partial<Data>, Partial<Data>>(source: Partial<Data>, target: Partial<Data>, ...targets: Partial<Data>[]): Partial<Data> // TData = Partial<Data> // TResult = Data console.log({ merged });
      » npm install object-deep-merge
    
Published   Oct 20, 2025
Version   2.0.0
Author   Forcir Engineering
🌐
npm
npmjs.com › package › deepmerge
deepmerge - npm
March 16, 2023 - A library for deep (recursive) merging of Javascript objects. Latest version: 4.3.1, last published: 3 years ago. Start using deepmerge in your project by running `npm i deepmerge`. There are 12608 other projects in the npm registry using deepmerge.
      » npm install deepmerge
    
Published   Mar 16, 2023
Version   4.3.1
Top answer
1 of 2
50

TLDR: Magic! Try the Playground

So, this is a tricky question. Not so much because of the merge requirements, but because of the edge cases. Getting the low hanging fruit took <20 minutes. Making sure it works everywhere took a couple more hours... and tripled the length. Unions are tricky!

  1. What is an optional property? In { a: 1 | undefined, b?: 1 } is a an optional property? Some people say yes. Others no. Personally, I only include b in the optional list.

  2. How do you handle unions? What is the output of Merge<{}, { a: 1} | { b: 2 }>? I think the type that makes the most sense is { a?: 1 } | { b?: 2 }. What about Merge<string, { a: 1 }>? If you don't care at all about unions, this is easy... if you do, then you have to consider all these. (What I chose in parens)

    1. Merge<never, never> (never)
    2. Merge<never, { a: 1 }> ({ a?: 1 })
    3. Merge<string, { a: 1 }> (string | { a?: 1 })
    4. Merge<string | { a: 1 }, { a: 2 }> (string | { a: 1 | 2 })

Let's figure out this type, starting with the helpers.

I had an inkling as soon as I thought about unions that this type was going to become complex. TypeScript doesn't have a nice builtin way to test type equality, but we can write a helper type that causes a compiler error if two types aren't equal.

(Note: The Test type could be improved, it could allow types to pass that aren't equivalent, but it is sufficient for our uses here while remaining pretty simple)

type Pass = 'pass';
type Test<T, U> = [T] extends [U]
    ? [U] extends [T]
        ? Pass
        : { actual: T; expected: U }
    : { actual: T; expected: U };

function typeAssert<T extends Pass>() {}

We can use this helper like this:

// try changing Partial to Required
typeAssert<Test<Partial<{ a: 1 }>, { a?: 1 }>>();

Next, we'll need two helper types. One to get all required keys of an object, and one to get the optional keys. First, some tests to describe what we are after:

typeAssert<Test<RequiredKeys<never>, never>>();
typeAssert<Test<RequiredKeys<{}>, never>>();
typeAssert<Test<RequiredKeys<{ a: 1; b: 1 | undefined }>, 'a' | 'b'>>();

typeAssert<Test<OptionalKeys<never>, never>>();
typeAssert<Test<OptionalKeys<{}>, never>>();
typeAssert<Test<OptionalKeys<{ a?: 1; b: 1, c: undefined }>, 'a'>>();

There are two things to note here. First, *Keys<never> is never. This is important because we will be using these helpers in unions later, and if the object is never it shouldn't contribute any keys. Second, none of these tests include union checks. Considering how important I said unions were, this might surprise you. However, these types are only used after all unions are distributed, so their behavior there doesn't matter (though if you include these in your project, you might want to look at said behavior, it is different that you'd probably expect for RequiredKeys due to how its written)

These types pass the given checks:

type OptionalKeys<T> = {
    [K in keyof T]-?: T extends Record<K, T[K]> ? never : K;
}[keyof T;

type RequiredKeys<T> = {
    [K in keyof T]-?: T extends Record<K, T[K]> ? K : never;
}[keyof T] & keyof T;

Couple notes about these:

  1. Use -? to remove optionality of properties, this lets us avoid a wrapper of Exclude<..., undefined>
  2. T extends Record<K, T[K]> works because { a?: 1 } does not extend { a: 1 | undefined }. I went through a few iterations before finally settling on this. You can also detect optionality with another mapped type as jcalz does here.
  3. In version 3.8.3, TypeScript can correctly infer that the return type of OptionalKeys is assignable to keyof T. It cannot, however, detect the same for RequiredKeys. Intersecting with keyof T fixes this.

Now that we have these helpers, we can define two more types that represent your business logic. We need RequiredMergeKeys<T, U> and OptionalMergeKeys<T, U>.

type RequiredMergeKeys<T, U> = RequiredKeys<T> & RequiredKeys<U>;

type OptionalMergeKeys<T, U> =
    | OptionalKeys<T>
    | OptionalKeys<U>
    | Exclude<RequiredKeys<T>, RequiredKeys<U>>
    | Exclude<RequiredKeys<U>, RequiredKeys<T>>;

And some tests to make sure these behave as expected:

typeAssert<Test<OptionalMergeKeys<never, {}>, never>>();
typeAssert<Test<OptionalMergeKeys<never, { a: 1 }>, 'a'>>();
typeAssert<Test<OptionalMergeKeys<never, { a?: 1 }>, 'a'>>();
typeAssert<Test<OptionalMergeKeys<{}, {}>, never>>();
typeAssert<Test<OptionalMergeKeys<{ a: 1 }, { b: 2 }>, 'a' | 'b'>>();
typeAssert<Test<OptionalMergeKeys<{}, { a?: 1 }>, 'a'>>();

typeAssert<Test<RequiredMergeKeys<never, never>, never>>();
typeAssert<Test<RequiredMergeKeys<never, {}>, never>>();
typeAssert<Test<RequiredMergeKeys<never, { a: 1 }>, never>>();
typeAssert<Test<RequiredMergeKeys<{ a: 0 }, { a: 1 }>, 'a'>>();

Now that we have these, we can define the merge of two objects, ignoring primitives and unions for the moment. This calls the top level Merge type that we haven't defined yet to handle primitives and unions of the members.

type MergeNonUnionObjects<T, U> = {
    [K in RequiredMergeKeys<T, U>]: Merge<T[K], U[K]>;
} & {
    [K in OptionalMergeKeys<T, U>]?: K extends keyof T
        ? K extends keyof U
            ? Merge<Exclude<T[K], undefined>, Exclude<U[K], undefined>>
            : T[K]
        : K extends keyof U
        ? U[K]
        : never;
};

(I didn't write specific tests here because I had them for the next level up)

We need to handle both unions and non-objects. Let's handle unions of objects next. Per the discussion earlier, we need to distribute over all types and merge them individually. This is pretty straightforward.

type MergeObjects<T, U> = [T] extends [never]
    ? U extends any
        ? MergeNonUnionObjects<T, U>
        : never
    : [U] extends [never]
    ? T extends any
        ? MergeNonUnionObjects<T, U>
        : never
    : T extends any
    ? U extends any
        ? MergeNonUnionObjects<T, U>
        : never
    : never;

Note that we have extra checks for [T] extends [never] and [U] extends [never]. This is because never in a distributive clause is like for (let i = 0; i < 0; i++), it will never enter the "body" of the conditional and will therefore return never, but we only want never if both types are never.

We're almost there! We can now handle merging objects, which is the hardest part of this problem. All that's left is to handle primitives, which we can do by just forming a union of all possible primitives and excluding primitives to the types passed to MergeObjects.

type Primitive = string | number | boolean | bigint | symbol | null | undefined;

type Merge<T, U> =
    | Extract<T | U, Primitive>
    | MergeObjects<Exclude<T, Primitive>, Exclude<U, Primitive>>;

And with that type, we're done! Merge behaves as desired, in only 50 or so lines of uncommented insanity.

... or are we? @petroni mentioned in the comments that this type doesn't play well with arrays that are present in both objects. There are a few different ways to handle this, particularly because TypeScript's tuple types have become increasingly flexible. Properly merging [1, 2] and [3] should probably give [1 | 3, 2?]... but doing that is at least as complicated as what we've already done. A much simpler solution is to ignore tuples entirely, and always produce an array, so this example would produce (1 | 2 | 3)[].

A final note on produced types:

The resulting type from Merge right now is correct, but it isn't as readable as it could be. Right now hovering over the resulting type will show an intersection and inner objects with have Merge wrapped around them instead of showing the result. We can fix this by introducing an Expand type that forces TS to expand everything into a single object.

type Expand<T> = T extends Primitive ? T : { [K in keyof T]: T[K] };

Now just modify MergeNonUnionObjects to call Expand. Where this is necessary is somewhat trial and error. You can play around with including it, or not, to get a type display that works for you.

type MergeNonUnionObjects<T, U> = Expand<
    {
        [K in RequiredMergeKeys<T, U>]: Expand<Merge<T[K], U[K]>>;
    } & {
        [K in OptionalMergeKeys<T, U>]?: K extends keyof T
            ? K extends keyof U
                ? Expand<Merge<
                    Exclude<T[K], undefined>,
                    Exclude<U[K], undefined>
                >>
                : T[K]
            : K extends keyof U
            ? U[K]
            : never;
    }
>;

Check it out in the playground which includes all the tests I used to validate the results.

2 of 2
1

I tried to workaround this question with the MergeDeep type from the type-fest library and ramda's mergeDeepWith method.

demo code

import { mergeDeepWith } from "ramda";
import type { MergeDeep, MergeDeepOptions } from "type-fest";

type Foo = {
  life: number;
  items: string[];
  users: { id: number; name: string }[];
  a: { b: string; c: boolean; d: number[] };
};

type Bar = {
  name: string;
  items: number[];
  users: { id: number; name: string }[];
  a: { b: number; d: boolean[] };
};

type FooBar = MergeDeep<Foo, Bar>;

const mergeDeep = <Source, Destination, Options extends MergeDeepOptions = {}>(
  source: Source,
  destination: Destination,
  options?: Options
): MergeDeep<Source, Destination, Options> => {
  // https://github.com/sindresorhus/type-fest/blob/main/source/merge-deep.d.ts#L416-L456
  // Make your implementation ...
  const mergedObj = mergeDeepWith<Source, Destination>(
    (x, y) => {
      // https://github.com/denoland/deno/blob/main/ext/node/polyfills/util.ts#L30-L32
      if (Array.isArray(x) && Array.isArray(y)) {
        return [...x, ...y];
      }
      if (x) return x;
      if (y) return y;
      return null;
    },
    source,
    destination
  );
  return mergedObj;
};

const a: Foo = {
  life: 1,
  items: ["a"],
  users: [
    {
      id: 1,
      name: "user1",
    },
    {
      id: 2,
      name: "user2",
    },
  ],
  a: {
    b: "@",
    c: false,
    d: [1, 2, 3],
  },
};

const b: Bar = {
  name: "bar",
  items: [4, 5, 6],
  users: [
    {
      id: 3,
      name: "user3",
    },
    {
      id: 4,
      name: "user4",
    },
  ],
  a: {
    b: 111,
    d: [true, false],
  },
};

const result = mergeDeep<Foo, Bar>(a, b);

console.log(result);
🌐
GitHub
gist.github.com › ahtcx › 0cd94e62691f539160b32ecda18af3d6
Deep-Merge JavaScript objects with ES6 · GitHub
This solution is for iteratively merge any number of objects (it's typed with TypeScript, you might need to remove typings before using on regular JS projects): export function deepMerge(...objects: object[]) { const isObject = (obj: any) => ...
🌐
Webdevtutor
webdevtutor.net › blog › typescript-deepmerge
Exploring Typescript Deepmerge Functionality
In conclusion, deepmerge is a valuable library for merging objects deeply in Typescript.
🌐
Npm
npm.io › search › keyword:deep-merge
Deep-merge | npm.io
javascriptmergedeepmergerecursivelyobject-assigndeep-assignnested-assigntypescriptdeep-mergemerge-object6.0.4 • Published 1 year ago · A TypeScript deep merge function.