How to deep merge instead of shallow merge?
Asked Answered
A

49

635

Both Object.assign and Object spread only do a shallow merge.

An example of the problem:

// No object nesting
const x = { a: 1 }
const y = { b: 1 }
const z = { ...x, ...y } // { a: 1, b: 1 }

The output is what you'd expect. However if I try this:

// Object nesting
const x = { a: { a: 1 } }
const y = { a: { b: 1 } }
const z = { ...x, ...y } // { a: { b: 1 } }

Instead of

{ a: { a: 1, b: 1 } }

you get

{ a: { b: 1 } }

x is completely overwritten because the spread syntax only goes one level deep. This is the same with Object.assign().

Is there a way to do this?

Analog answered 14/1, 2015 at 6:7 Comment(8)
is deep merging same as copying properties from one object to another?Dulcie
No, as object properties should not be overwritten, rather each child object should be merged into the same child on the target if it already exists.Analog
ES6 is finalized and new features are no longer added, AFAIK.Organelle
See What is the most efficient way to clone an object?Hoyos
@Hoyos requires jQuery though...Uredo
const merge = (p, c) => Object.keys(p).forEach(k => !!p[k] && p[k].constructor === Object ? merge(p[k], c[k]) : c[k] = p[k])Carcinoma
you could look on the following GitHub link to get the solution with short lines of codes https://gist.github.com/ahtcx/0cd94e62691f539160b32ecda18af3d6Ize
@Xaqron's one-line was more than enough for me, with just a simple fix for undefined properties in the destination object. I also returned the merged object, so to use inline const merge = (p, c) => Object.keys(p).forEach(k => (p[k] instanceof Object && c[k] instanceof Object) ? merge(p[k], c[k]) : c[k] = p[k]) ?? cLongstanding
P
301

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.

Parotitis answered 12/1, 2016 at 17:15 Comment(15)
if your object graph contains cycles that will lead to infinite recursionTenaculum
item !== null shouldn't be needed inside isObject, because item is already checked for truthiness at the beginning of the conditionBahaism
@Matteo derp, completely missed that, standard code blindness ;p edited, thanksParotitis
Extended with ability to assign array properties with deep nested objects: jsbin.com/tisodigahi/2/edit?js,outputArsonist
Why write this: Object.assign(target, { [key]: {} }) if it could simply be target[key] = {}?Juncaceous
...and target[key] = source[key] instead of Object.assign(target, { [key]: source[key] });Juncaceous
null is an object so it's better to replace item && typeof item === 'object' with item !== null && typeof item === 'object'Coxalgia
This does not support any non-plain objects in target. For instance, mergeDeep({a: 3}, {a: {b: 4}}) will result in an augmented Number object, which is clearly not desired. Also, isObject does not accept arrays, but accepts any other native object type, such as Date, which should not be deep copied.Widener
In isObject you should check for !!item in order to detect also null object. Otherwise passing null will return null instead of a boolean false.Geisel
To address the comment about mergeDeep({a: 3}, {a: {b: 4}}) not working I changed if (!target[key]) Object.assign(target, { [key]: {} }); to if (!isObject(target[key])) Object.assign(target, { [key]: {} });Athenaathenaeum
This example seems to be from here blog.devgenius.io/… which contains a full explanation of code.Rackley
this does not merge arrays and instead just blows away the old one.Kwabena
"You'll find an immutable version of this in the answer below." There are 56 other answers to this question, and the relative positions of the answers depends on the sorting. Please edit the answer to change "the answer" to a link to the answer referenced.Polytechnic
@Rackley Other way around, if we are to believe the dates; This post was written in 2016; that blog post was written in 2021... It would be good of the author of that blog post to credit this answer if it's where they got the code.Polytechnic
As @Widener and @Athenaathenaeum pointed out, you probably want if (!isObject(target[key])) instead of if (!target[key]), so mergeDeep({a: 3}, {a: {b: 4}}) results in {a: {b:4}} instead of {a: 3}, which doesn't merge the field.Tupungato
A
243

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>
Apophyge answered 4/1, 2017 at 23:5 Comment(9)
Hey people, this is the simplest and most beautiful solution. Lodash is awesome, they should include it as core js objectMartins
Shouldn't the result be { 'a': [{ 'b': 2 }, { 'c': 3 }, { 'd': 4 }, { 'e': 5 }] }?Kraemer
Good question. That might be a separate question or one for the Lodash maintainers.Apophyge
The result { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] } is correct, because we're merging elements of an array. The element 0 of object.a is {b: 2}, the element 0 of other.a is {c: 3}. When these two are merged because they have the same array index, the result is { 'b': 2, 'c': 3 }, which is the element 0 in the new object.Bromeosin
I prefer this one, it's 6x smaller gzipped.Idiophone
@J.Hesters to achieve what you describe there's another method by lodash: mergeWithMartlet
adding dependencies is not beautifulScaliger
@Scaliger reinventing the wheel is not beautifulRodriquez
This is not a deep merge. Keys are overwritten.Noma
T
127

The problem is non-trivial when it comes to host objects or any kind of object that's more complex than a bag of values

  • do you invoke a getter to obtain a value or do you copy over the property descriptor?
  • what if the merge target has a setter (either own property or in its prototype chain)? Do you consider the value as already-present or call the setter to update the current value?
  • do you invoke own-property functions or copy them over? What if they're bound functions or arrow functions depending on something in their scope chain at the time they were defined?
  • what if it's something like a DOM node? You certainly don't want to treat it as simple object and just deep-merge all its properties over into
  • how to deal with "simple" structures like arrays or maps or sets? Consider them already-present or merge them too?
  • how to deal with non-enumerable own properties?
  • what about new subtrees? Simply assign by reference or deep clone?
  • how to deal with frozen/sealed/non-extensible objects?

Another thing to keep in mind: Object graphs that contain cycles. It's usually not difficult to deal with - simply keep a Set of already-visited source objects - but often forgotten.

You probably should write a deep-merge function that only expects primitive values and simple objects - at most those types that the structured clone algorithm can handle - as merge sources. Throw if it encounters anything it cannot handle or just assign by reference instead of deep merging.

In other words, there is no one-size-fits-all algorithm, you either have to roll your own or look for a library method that happens to cover your use-cases.

Tenaculum answered 31/1, 2015 at 5:35 Comment(3)
excuses for V8 devs to not implement a secure "document state" transferBateman
You raise many good issues and I would have loved to see an implementation of your recommendation. So I tried to make one below. Could you please have a look and comment? https://mcmap.net/q/36420/-how-to-deep-merge-instead-of-shallow-mergeFigure
Another thing to keep in mind: The solutions on this page are vulnerable to prototype pollution: learn.snyk.io/lessons/prototype-pollution/javascript Many existing libraries are protected against this.Confidant
I
109

Here is an immutable (does not modify the inputs) version of @Salakar's answer. Useful if you're doing functional programming type stuff.

export function isObject(item) {
  return (item && typeof item === 'object' && !Array.isArray(item));
}

export default function mergeDeep(target, source) {
  let output = Object.assign({}, target);
  if (isObject(target) && isObject(source)) {
    Object.keys(source).forEach(key => {
      if (isObject(source[key])) {
        if (!(key in target))
          Object.assign(output, { [key]: source[key] });
        else
          output[key] = mergeDeep(target[key], source[key]);
      } else {
        Object.assign(output, { [key]: source[key] });
      }
    });
  }
  return output;
}
Invigorate answered 11/5, 2016 at 13:43 Comment(11)
@torazaburo see previous post by me for the isObject functionParotitis
updated it. after some testing i found a bug with the deeply nested objectsInvigorate
What's the difference between { [key]: source[key] } and { key: source[key] } ? I notice that both you and Salakar used [key] for the property nameFallingout
Its a computed property name, the first will use the value of key as the property name, the later will make "key" the property name. See: es6-features.org/#ComputedPropertyNamesInvigorate
Using this is still creating a mutable input. const myInput = { test: 123 } let toMerge; mergeDeep(toMerge, myInput); myInput.test = 789; console.log(myInput.test) // 789Nicker
Yeah, it doesn't guarantee the result is immutable, it just doesn't modify it's inputs so its functional immutable compatible. Also, you should be testing toMerge in your example not myInput...Invigorate
in isObject you don't need to check && item !== null at the end, because the line starts with item &&, no?Segura
If source has nested child objects deeper than target, those objects will still reference the same values in mergedDeep's output (I think). E.g. const target = { a: 1 }; const source = { b: { c: 2 } }; const merged = mergeDeep(target, source); merged.b.c; // 2 source.b.c = 3; merged.b.c; // 3 Is this an issue? It doesn't mutate the inputs, but any future mutations to the inputs could mutate the output, and vice versa w/ mutations to output mutating inputs. For what it's worth, though, ramda's R.merge() has the same behavior.Warila
Yeah, if your using React this is a good thing as components that take those deeper objects as props will not re-render.Invigorate
While this works great, it does not concatenate any arrays inside the objects, if they exist. Adding another else block between the if (isObject(source[key])) {...} and before the } else { block fixes this: } else if (Array.isArray(source[key])) { if (!(key in target)) { output[key] = source[key]; } else { output[key] = output[key].concat(source[key]);}Trella
yeah, this merges keys not values unless its a dict. Feel free to update the answerInvigorate
O
108

Update 2022:

I created mergician to address the various merge/clone requirements discussed in the comments and handle more advanced scenarios. It is based on the same concept as my original answer (below) but offers a more robust solution when needed:

Unlike native methods and other utilities, Mergician faithfully clones and merges objects by properly handling descriptor values, accessor functions, and prototype properties while offering advanced options for customizing the clone/merge process.

Notably, mergician is significantly smaller (1.5k min+gzip) than similar utilities like lodash.merge (5.1k min+gzip).


Original answer:

Since this issue is still active, here's another approach:

  • ES6/2015
  • Immutable (does not modify original objects)
  • Handles arrays (concatenates them)

/**
* Performs a deep merge of objects and returns new object. Does not modify
* objects (immutable) and merges arrays via concatenation.
*
* @param {...object} objects - Objects to merge
* @returns {object} New object with merged key/values
*/
function mergeDeep(...objects) {
  const isObject = obj => obj && typeof obj === 'object';
  
  return objects.reduce((prev, obj) => {
    Object.keys(obj).forEach(key => {
      const pVal = prev[key];
      const oVal = obj[key];
      
      if (Array.isArray(pVal) && Array.isArray(oVal)) {
        prev[key] = pVal.concat(...oVal);
      }
      else if (isObject(pVal) && isObject(oVal)) {
        prev[key] = mergeDeep(pVal, oVal);
      }
      else {
        prev[key] = oVal;
      }
    });
    
    return prev;
  }, {});
}

// Test objects
const obj1 = {
  a: 1,
  b: 1, 
  c: { x: 1, y: 1 },
  d: [ 1, 1 ]
}
const obj2 = {
  b: 2, 
  c: { y: 2, z: 2 },
  d: [ 2, 2 ],
  e: 2
}
const obj3 = mergeDeep(obj1, obj2);

// Out
console.log(obj3);
Oconner answered 12/1, 2018 at 1:15 Comment(9)
This is nice. However when we have array with repeated elements these are concatenated (there are repeated elements). I adapted this to take a parameter (arrays unique: true/false).Owens
To make the arrays unique you can change prev[key] = pVal.concat(...oVal); to prev[key] = [...pVal, ...oVal].filter((element, index, array) => array.indexOf(element) === index);Deglutinate
Glorious. This one demonstrates also that arrays get merged, which is what I was looking for.Putumayo
Yep, the @CplLL solution is said to be immutable but uses actual object mutability inside the function while using reduce doesn't.Brow
Does not handle Arrays as arguments, only Objects, not a full solution :(Elongate
The isObject check is naive. Use this instead: (obj+"") === "[object Object]"Elongate
Alternative es6 solution for unique arrays. Change prev[key] = pVal.concat(...oVal); to prev[key] = [...new Set([...oVal, ...pVal])]; Reference: https://mcmap.net/q/36576/-remove-duplicate-values-from-js-arrayLuminance
I read all the answers, this library is the most suitable for me, and there are suitable configuration items, which can be configured according to the actual situation. The only regret is that there is no typescript support, I hope it will be improved in the future. thanksNarcosis
I with the reduce call when creating the newObj could take a configurable initialObject instead of a hard-coded {}. What if I want to call Object.assign on an element and set the style attributes? In this case, I would want a mutable version that take the element in question as the initial value. e.g. document.body.append(Object.assign(document.createElement('div'), { className: 'point', style: { top: `${e.clientY}px`, left: `${e.clientX}px` } }))Overglaze
F
48

I know there's a lot of answers already and as many comments arguing they won't work. The only consensus is that it's so complicated that nobody made a standard for it. However, most of accepted answers in SO expose "simple tricks" that are widely used. So, for all of us like me who are no experts but want to write safer code by grasping a little more about javascript's complexity, I'll try to shed some light.

Before getting our hands dirty, let me clarify 2 points:

  • [DISCLAIMER] I propose a function below that tackles how we deep loop into javascript objects for copy and illustrates what is generally too shortly commented. It is not production-ready. For sake of clarity, I have purposedly left aside other considerations like circular objects (track by a set or unconflicting symbol property), copying reference value or deep clone, immutable destination object (deep clone again?), case-by-case study of each type of objects, get/set properties via accessors... Also, I did not test performance -although it's important- because it's not the point here either.
  • I'll use copy or assign terms instead of merge. Because in my mind a merge is conservative and should fail upon conflicts. Here, when conflicting, we want the source to overwrite the destination. Like Object.assign does.

Answers with for..in or Object.keys are misleading

Making a deep copy seems so basic and common practice that we expect to find a one-liner or, at least, a quick win via simple recursion. We don't expect we should need a library or write a custom function of 100 lines.

When I first read Salakar's answer, I genuinely thought I could do better and simpler (you can compare it with Object.assign on x={a:1}, y={a:{b:1}}). Then I read the8472's answer and I thought... there is no getting away so easily, improving already given answers won't get us far.

Let's let deep copy and recursive aside an instant. Just consider how (wrongly) people parse properties to copy a very simple object.

const y = Object.create(
    { proto : 1 },
    { a: { enumerable: true, value: 1},
      [Symbol('b')] : { enumerable: true, value: 1} } )

Object.assign({},y)
> { 'a': 1, Symbol(b): 1 } // All (enumerable) properties are copied

((x,y) => Object.keys(y).reduce((acc,k) => Object.assign(acc, { [k]: y[k] }), x))({},y)
> { 'a': 1 } // Missing a property!

((x,y) => {for (let k in y) x[k]=y[k];return x})({},y)
> { 'a': 1, 'proto': 1 } // Missing a property! Prototype's property is copied too!

Object.keys will omit own non-enumerable properties, own symbol-keyed properties and all prototype's properties. It may be fine if your objects don't have any of those. But keep it mind that Object.assign handles own symbol-keyed enumerable properties. So your custom copy lost its bloom.

for..in will provide properties of the source, of its prototype and of the full prototype chain without you wanting it (or knowing it). Your target may end up with too many properties, mixing up prototype properties and own properties.

If you're writing a general purpose function and you're not using Object.getOwnPropertyDescriptors, Object.getOwnPropertyNames, Object.getOwnPropertySymbols or Object.getPrototypeOf, you're most probably doing it wrong.

Things to consider before writing your function

First, make sure you understand what a Javascript object is. In Javascript, an object is made of its own properties and a (parent) prototype object. The prototype object in turn is made of its own properties and a prototype object. And so on, defining a prototype chain.

A property is a pair of key (string or symbol) and descriptor (value or get/set accessor, and attributes like enumerable).

Finally, there are many types of objects. You may want to handle differently an object Object from an object Date or an object Function.

So, writing your deep copy, you should answer at least those questions:

  1. What do I consider deep (proper for recursive look up) or flat?
  2. What properties do I want to copy? (enumerable/non-enumerable, string-keyed/symbol-keyed, own properties/prototype's own properties, values/descriptors...)

For my example, I consider that only the object Objects are deep, because other objects created by other constructors may not be proper for an in-depth look. Customized from this SO.

function toType(a) {
    // Get fine type (object, array, function, null, error, date ...)
    return ({}).toString.call(a).match(/([a-z]+)(:?\])/i)[1];
}

function isDeepObject(obj) {
    return "Object" === toType(obj);
}

And I made an options object to choose what to copy (for demo purpose).

const options = {nonEnum:true, symbols:true, descriptors: true, proto:true};

Proposed function

You can test it in this plunker.

function deepAssign(options) {
    return function deepAssignWithOptions (target, ...sources) {
        sources.forEach( (source) => {

            if (!isDeepObject(source) || !isDeepObject(target))
                return;

            // Copy source's own properties into target's own properties
            function copyProperty(property) {
                const descriptor = Object.getOwnPropertyDescriptor(source, property);
                //default: omit non-enumerable properties
                if (descriptor.enumerable || options.nonEnum) {
                    // Copy in-depth first
                    if (isDeepObject(source[property]) && isDeepObject(target[property]))
                        descriptor.value = deepAssign(options)(target[property], source[property]);
                    //default: omit descriptors
                    if (options.descriptors)
                        Object.defineProperty(target, property, descriptor); // shallow copy descriptor
                    else
                        target[property] = descriptor.value; // shallow copy value only
                }
            }

            // Copy string-keyed properties
            Object.getOwnPropertyNames(source).forEach(copyProperty);

            //default: omit symbol-keyed properties
            if (options.symbols)
                Object.getOwnPropertySymbols(source).forEach(copyProperty);

            //default: omit prototype's own properties
            if (options.proto)
                // Copy souce prototype's own properties into target prototype's own properties
                deepAssign(Object.assign({},options,{proto:false})) (// Prevent deeper copy of the prototype chain
                    Object.getPrototypeOf(target),
                    Object.getPrototypeOf(source)
                );

        });
        return target;
    }
}

That can be used like this:

const x = { a: { a: 1 } },
      y = { a: { b: 1 } };
deepAssign(options)(x,y); // { a: { a: 1, b: 1 } }
Figure answered 2/2, 2018 at 9:27 Comment(2)
Thank God! This works for me! Am I correct to state that "target is always the "smaller" object"? ThanksHis
This is probably my favorite answer as it takes into account all environmental factors. However, there is a bug in the code you should be aware of. Essentially if the target has a non-"deep"-object property but the source's is, it doesn't deep clone the source and so modifying the source ends up modifying the target also. Can be fixed fairly easily. Also, not a fan options model. const target = { foo: 4 }; const source = { foo: { bar: 5 } }; deepAssign({})(target, source); console.log(target); // target.foo.bar === 5 source.foo.bar = 6; console.log(target); // target.foo.bar === 6Bassarisk
R
41

If you want to have a one liner without requiring a huge library like lodash, I suggest you to use deepmerge (npm install deepmerge) or deepmerge-ts (npm install deepmerge-ts).

deepmerge also comes with typings for TypeScript and is more stable (since it's older), but deepmerge-ts is also available for Deno and is faster by design, although written in TypeScript as the name implies.

Once imported you can do

deepmerge({ a: 1, b: 2, c: 3 }, { a: 2, d: 3 });

to get

{ a: 2, b: 2, c: 3, d: 3 }

This works nicely with complex objects and arrays. A real all-rounder solution this is.

Rodriquez answered 2/8, 2019 at 18:27 Comment(5)
Been looking for hours, this saved the day, was able to merge deep objects, and as you said an all-rounder, cheers!Marvismarwin
You don't have to require the entire lodash library. You can require just the parts you need: const merge = require('lodash.merge');Alyssaalyssum
looked into their source code and didn't like their typings as they used as / anyWinson
@Winson I agree. Since deepmerge-ts is preferred, I decided to open an issue about it. The usage of any is very minimal, but should be avoided. The older deepmerge should not be bothered, since it would only be preferred for JavaScript environments.Rodriquez
Thank you for this deepmerge-ts lib. I'm using it now in my deno.jsonc config file update cli: github.com/codemonument/deno_update_denoconfigPolemics
O
20

Here, straight forward;

a simple solution that works like Object.assign just deep, and works for an array, without need any modification.

function deepAssign(target, ...sources) {
  for (source of sources) {
    for (let k in source) {
      let vs = source[k], vt = target[k]
      if (Object(vs) == vs && Object(vt) === vt) {
        target[k] = deepAssign(vt, vs)
        continue
      }
      target[k] = source[k]
    }
  }
  return target
}

x = { a: { a: 1 }, b: [1,2] }
y = { a: { b: 1 }, b: [3] }
z = { c: 3, b: [,,,4] }
x = deepAssign(x, y, z)

console.log(JSON.stringify(x) === JSON.stringify({
  "a": {
    "a": 1,
    "b": 1
  },
  "b": [ 1, 2, null, 4 ],
  "c": 3
}))

Edit: I answer somewhere else about a new method for deep comparing 2 objects. that method can use also for a deep merging. If you want implantation put a comment https://mcmap.net/q/36579/-generic-deep-diff-between-two-objects

Outdated answered 23/4, 2020 at 19:2 Comment(4)
You should use more types varieties in your test case (new Date(), a(){}, null, undefined, 0).Elongate
Found major bug - jsbin.com/javefudife/1/edit?html,js,consoleElongate
what the bug? the example you use is very simple. and checking it in the console brings the right result . jsbin look buggiOutdated
Throws an error: Object is not iterable with this given input: { "CommandWorkflows": { "businessRules": [{ "arrayParsing": [{ "characterArrayParsing": [{ "Workflow": [{ "$": { "Name": "doesArrayContainCharacter", "Value": "cmdgen bizRul,doesArrayContainCharacter,$,[the|answer|to|life|the|universe|and|everything|is|$42] 4"}}]}]}]}]}}Stipule
G
17

Many answers use tens of lines of code, or require adding a new library to the project, but if you use recursion, this is just 4 lines of code.

function merge(current, updates) {
  for (key of Object.keys(updates)) {
    if (!current.hasOwnProperty(key) || typeof updates[key] !== 'object') current[key] = updates[key];
    else merge(current[key], updates[key]);
  }
  return current;
}
console.log(merge({ a: { a: 1 } }, { a: { b: 1 } }));

Arrays handling: The above version overwrites old array values with new ones. If you want it to keep the old array values and add the new ones, just add a else if (current[key] instanceof Array && updates[key] instanceof Array) current[key] = current[key].concat(updates[key]) block above the else statament and you're all set.

Goldfilled answered 23/11, 2019 at 14:7 Comment(1)
I like it but it needs a simple undefined check for 'current' or else {foo: undefined} does not merge. Just add an if(current) before the for loop.Pelecypod
C
15

Here is TypeScript implementation:

export const mergeObjects = <T extends object = object>(target: T, ...sources: T[]): T  => {
  if (!sources.length) {
    return target;
  }
  const source = sources.shift();
  if (source === undefined) {
    return target;
  }

  if (isMergebleObject(target) && isMergebleObject(source)) {
    Object.keys(source).forEach(function(key: string) {
      if (isMergebleObject(source[key])) {
        if (!target[key]) {
          target[key] = {};
        }
        mergeObjects(target[key], source[key]);
      } else {
        target[key] = source[key];
      }
    });
  }

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

const isObject = (item: any): boolean => {
  return item !== null && typeof item === 'object';
};

const isMergebleObject = (item): boolean => {
  return isObject(item) && !Array.isArray(item);
};

And Unit Tests:

describe('merge', () => {
  it('should merge Objects and all nested Ones', () => {
    const obj1 = { a: { a1: 'A1'}, c: 'C', d: {} };
    const obj2 = { a: { a2: 'A2'}, b: { b1: 'B1'}, d: null };
    const obj3 = { a: { a1: 'A1', a2: 'A2'}, b: { b1: 'B1'}, c: 'C', d: null};
    expect(mergeObjects({}, obj1, obj2)).toEqual(obj3);
  });
  it('should behave like Object.assign on the top level', () => {
    const obj1 = { a: { a1: 'A1'}, c: 'C'};
    const obj2 = { a: undefined, b: { b1: 'B1'}};
    expect(mergeObjects({}, obj1, obj2)).toEqual(Object.assign({}, obj1, obj2));
  });
  it('should not merge array values, just override', () => {
    const obj1 = {a: ['A', 'B']};
    const obj2 = {a: ['C'], b: ['D']};
    expect(mergeObjects({}, obj1, obj2)).toEqual({a: ['C'], b: ['D']});
  });
  it('typed merge', () => {
    expect(mergeObjects<TestPosition>(new TestPosition(0, 0), new TestPosition(1, 1)))
      .toEqual(new TestPosition(1, 1));
  });
});

class TestPosition {
  constructor(public x: number = 0, public y: number = 0) {/*empty*/}
}
Coxalgia answered 27/10, 2017 at 10:47 Comment(0)
J
12

The deepmerge npm package appears to be the most widely used library for solving this problem: https://www.npmjs.com/package/deepmerge

Jillene answered 10/8, 2018 at 13:53 Comment(0)
R
11

I would like to present a pretty simple ES5 alternative. The function gets 2 parameters - target and source that must be of type "object". Target will be the resulting object. Target keeps all its original properties but their values may be modified though.

function deepMerge(target, source) {
if(typeof target !== 'object' || typeof source !== 'object') return false; // target or source or both ain't objects, merging doesn't make sense
for(var prop in source) {
  if(!source.hasOwnProperty(prop)) continue; // take into consideration only object's own properties.
  if(prop in target) { // handling merging of two properties with equal names
    if(typeof target[prop] !== 'object') {
      target[prop] = source[prop];
    } else {
      if(typeof source[prop] !== 'object') {
        target[prop] = source[prop];
      } else {
        if(target[prop].concat && source[prop].concat) { // two arrays get concatenated
          target[prop] = target[prop].concat(source[prop]);
        } else { // two objects get merged recursively
          target[prop] = deepMerge(target[prop], source[prop]); 
        } 
      }  
    }
  } else { // new properties get added to target
    target[prop] = source[prop]; 
  }
}
return target;
}

cases:

  • if target doesn't have a source property, target gets it;
  • if target does have a source property and target & source are not both objects (3 cases out of 4), target's property gets overriden;
  • if target does have a source property and both of them are objects/arrays (1 remaining case), then recursion happens merging two objects (or concatenation of two arrays);

also consider the following:

  1. array + obj = array
  2. obj + array = obj
  3. obj + obj = obj (recursively merged)
  4. array + array = array (concat)

It is predictable, supports primitive types as well as arrays and objects. Also as we can merge 2 objects, I think that we can merge more than 2 via reduce function.

take a look at an example (and play around with it if you want):

var a = {
   "a_prop": 1,
   "arr_prop": [4, 5, 6],
   "obj": {
     "a_prop": {
       "t_prop": 'test'
     },
     "b_prop": 2
   }
};

var b = {
   "a_prop": 5,
   "arr_prop": [7, 8, 9],
   "b_prop": 15,
   "obj": {
     "a_prop": {
       "u_prop": false
     },
     "b_prop": {
        "s_prop": null
     }
   }
};

function deepMerge(target, source) {
    if(typeof target !== 'object' || typeof source !== 'object') return false;
    for(var prop in source) {
    if(!source.hasOwnProperty(prop)) continue;
      if(prop in target) {
        if(typeof target[prop] !== 'object') {
          target[prop] = source[prop];
        } else {
          if(typeof source[prop] !== 'object') {
            target[prop] = source[prop];
          } else {
            if(target[prop].concat && source[prop].concat) {
              target[prop] = target[prop].concat(source[prop]);
            } else {
              target[prop] = deepMerge(target[prop], source[prop]); 
            } 
          }  
        }
      } else {
        target[prop] = source[prop]; 
      }
    }
  return target;
}

console.log(deepMerge(a, b));

There is a limitation - browser's call stack length. Modern browsers will throw an error at some really deep level of recursion (think of thousands of nested calls). Also you are free to treat situations like array + object etc. as you wish by adding new conditions and type checks.

Realty answered 8/5, 2018 at 22:4 Comment(0)
G
8

Is there a way to do this?

If npm libraries can be used as a solution, object-merge-advanced from yours truly allows to merge objects deeply and customise/override every single merge action using a familiar callback function. The main idea of it is more than just deep merging — what happens with the value when two keys are the same? This library takes care of that — when two keys clash, object-merge-advanced weighs the types, aiming to retain as much data as possible after merging:

object key merging weighing key value types to retain as much data as possible

First input argument's key is marked #1, second argument's — #2. Depending on each type, one is chosen for the result key's value. In diagram, "an object" means a plain object (not array etc).

When keys don't clash, they all enter the result.

From your example snippet, if you used object-merge-advanced to merge your code snippet:

const mergeObj = require("object-merge-advanced");
const x = { a: { a: 1 } };
const y = { a: { b: 1 } };
const res = console.log(mergeObj(x, y));
// => res = {
//      a: {
//        a: 1,
//        b: 1
//      }
//    }

It's algorithm recursively traverses all input object keys, compares and builds and returns the new merged result.

Grubstake answered 3/7, 2018 at 7:33 Comment(1)
where are dates and functions in this table infographic?Elongate
A
7

A simple solution with ES5 (overwrite existing value):

function merge(current, update) {
  Object.keys(update).forEach(function(key) {
    // if update[key] exist, and it's not a string or array,
    // we go in one level deeper
    if (current.hasOwnProperty(key) 
        && typeof current[key] === 'object'
        && !(current[key] instanceof Array)) {
      merge(current[key], update[key]);

    // if update[key] doesn't exist in current, or it's a string
    // or array, then assign/overwrite current[key] to update[key]
    } else {
      current[key] = update[key];
    }
  });
  return current;
}

var x = { a: { a: 1 } }
var y = { a: { b: 1 } }

console.log(merge(x, y));
Anastrophe answered 8/5, 2018 at 7:25 Comment(1)
just what i needed - es6 was causing problems in build - this es5 alternative is the bombLegislative
L
6

The following function makes a deep copy of objects, it covers copying primitive, arrays as well as object

 function mergeDeep (target, source)  {
    if (typeof target == "object" && typeof source == "object") {
        for (const key in source) {
            if (source[key] === null && (target[key] === undefined || target[key] === null)) {
                target[key] = null;
            } else if (source[key] instanceof Array) {
                if (!target[key]) target[key] = [];
                //concatenate arrays
                target[key] = target[key].concat(source[key]);
            } else if (typeof source[key] == "object") {
                if (!target[key]) target[key] = {};
                this.mergeDeep(target[key], source[key]);
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}
Leisure answered 16/1, 2018 at 7:22 Comment(0)
C
6

Most examples here seem too complex, I'm using one in TypeScript I created, I think it should cover most cases (I'm handling arrays as regular data, just replacing them).

const isObject = (item: any) => typeof item === 'object' && !Array.isArray(item);

export const merge = <A = Object, B = Object>(target: A, source: B): A & B => {
  const isDeep = (prop: string) =>
    isObject(source[prop]) && target.hasOwnProperty(prop) && isObject(target[prop]);
  const replaced = Object.getOwnPropertyNames(source)
    .map(prop => ({ [prop]: isDeep(prop) ? merge(target[prop], source[prop]) : source[prop] }))
    .reduce((a, b) => ({ ...a, ...b }), {});

  return {
    ...(target as Object),
    ...(replaced as Object)
  } as A & B;
};

Same thing in plain JS, just in case:

const isObject = item => typeof item === 'object' && !Array.isArray(item);

const merge = (target, source) => {
  const isDeep = prop => 
    isObject(source[prop]) && target.hasOwnProperty(prop) && isObject(target[prop]);
  const replaced = Object.getOwnPropertyNames(source)
    .map(prop => ({ [prop]: isDeep(prop) ? merge(target[prop], source[prop]) : source[prop] }))
    .reduce((a, b) => ({ ...a, ...b }), {});

  return {
    ...target,
    ...replaced
  };
};

Here are my test cases to show how you could use it

describe('merge', () => {
  context('shallow merges', () => {
    it('merges objects', () => {
      const a = { a: 'discard' };
      const b = { a: 'test' };
      expect(merge(a, b)).to.deep.equal({ a: 'test' });
    });
    it('extends objects', () => {
      const a = { a: 'test' };
      const b = { b: 'test' };
      expect(merge(a, b)).to.deep.equal({ a: 'test', b: 'test' });
    });
    it('extends a property with an object', () => {
      const a = { a: 'test' };
      const b = { b: { c: 'test' } };
      expect(merge(a, b)).to.deep.equal({ a: 'test', b: { c: 'test' } });
    });
    it('replaces a property with an object', () => {
      const a = { b: 'whatever', a: 'test' };
      const b = { b: { c: 'test' } };
      expect(merge(a, b)).to.deep.equal({ a: 'test', b: { c: 'test' } });
    });
  });

  context('deep merges', () => {
    it('merges objects', () => {
      const a = { test: { a: 'discard', b: 'test' }  };
      const b = { test: { a: 'test' } } ;
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: 'test' } });
    });
    it('extends objects', () => {
      const a = { test: { a: 'test' } };
      const b = { test: { b: 'test' } };
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: 'test' } });
    });
    it('extends a property with an object', () => {
      const a = { test: { a: 'test' } };
      const b = { test: { b: { c: 'test' } } };
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: { c: 'test' } } });
    });
    it('replaces a property with an object', () => {
      const a = { test: { b: 'whatever', a: 'test' } };
      const b = { test: { b: { c: 'test' } } };
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: { c: 'test' } } });
    });
  });
});

Please let me know if you think I'm missing some functionality.

Commissionaire answered 31/10, 2018 at 21:49 Comment(0)
O
5

If your are using ImmutableJS you can use mergeDeep :

fromJS(options).mergeDeep(options2).toJS();
Ornelas answered 23/1, 2017 at 11:26 Comment(0)
P
5

with reduce

export const merge = (objFrom, objTo) => Object.keys(objFrom)
    .reduce(
        (merged, key) => {
            merged[key] = objFrom[key] instanceof Object && !Array.isArray(objFrom[key])
                ? merge(objFrom[key], merged[key] ?? {})
                : objFrom[key]
            return merged
        }, { ...objTo }
    )
test('merge', async () => {
    const obj1 = { par1: -1, par2: { par2_1: -21, par2_5: -25 }, arr: [0,1,2] }
    const obj2 = { par1: 1, par2: { par2_1: 21 }, par3: 3, arr: [3,4,5] }
    const obj3 = merge3(obj1, obj2)
    expect(obj3).toEqual(
        { par1: -1, par2: { par2_1: -21, par2_5: -25 }, par3: 3, arr: [0,1,2] }
    )
})
Pepi answered 25/4, 2021 at 9:11 Comment(0)
O
4

We can use $.extend(true,object1,object2) for deep merging. Value true denotes merge two objects recursively, modifying the first.

$extend(true,target,object)

Onshore answered 21/2, 2017 at 4:45 Comment(3)
The asker never indicated that they are using jquery and appears to be asking for a native javascript solution.Turmel
This is a very simple way of doing this and it works. A viable solution that I would consider if I was the one asking this question. :)Quan
This is a very good answer but is missing a link to the source code to jQuery. jQuery has a lot of people working on the project and they have spent some time getting deep copying working properly. Also, the source code is fairly "simple": github.com/jquery/jquery/blob/master/src/core.js#L125 "Simple" is in quotes because it starts getting complicated when digging into jQuery.isPlainObject(). That exposes the complexity of determining whether or not something is a plain object, which most of the answers here miss by a long shot. Guess what language jQuery is written in?Disannul
B
4

Ramda which is a nice library of javascript functions has mergeDeepLeft and mergeDeepRight. Any of these work pretty well for this problem. Please take a look on the documentation here: https://ramdajs.com/docs/#mergeDeepLeft

For the specific example in question we can use:

import { mergeDeepLeft } from 'ramda'
const x = { a: { a: 1 } }
const y = { a: { b: 1 } }
const z = mergeDeepLeft(x, y)) // {"a":{"a":1,"b":1}}
Booby answered 18/4, 2019 at 1:24 Comment(0)
O
4

I didn't like any of the existing solutions. So, I went ahead and wrote my own.

Object.prototype.merge = function(object) {
    for (const key in object) {
        if (object.hasOwnProperty(key)) {
            if (typeof this[key] == 'object' && typeof object[key] == 'object') {
                this[key].merge(object[key]);
                continue;
            }

            this[key] = object[key];
        }
    }

    return this;
}

It would be used like this:

const object = {
    health: 100,
    position: {
        x: 0,
        y: 10
    }
};

object.merge({
    health: 99,
    position: {
        x: 10
    },
    extension: null
});

Which results in:

{
    health: 99,
    position: {
        x: 10,
        y: 10
    }
}

I hope this helps those of you who struggle to understand what's going on. I've seen a lot of meaningless variables being used on here.

Thanks

Opera answered 3/9, 2021 at 5:56 Comment(4)
This will merge only properties existing in this, maybe this.hasOwnProperty(key) should be object.hasOwnProperty(key)Cullen
@GiulianoCollacchioni Good catch! I was really tired when I made this, I wasn't really thinking with my brain.Opera
It still can't copy functions.Dynamometer
@VishalKumarSahu That's the point! Functions wouldn't necessarily need to be copied in any circumstance. But if you do have a reason for it, you can replace whatever is in the if statement that checks whether it's an object with a check with the function being in the original object's prototype. Something like this: (typeof this[key] == 'object' && typeof object[key] == 'object') || (typeof object[key] == 'function' && !(key in Reflect.getPrototypeOf(this))).Opera
O
3

I was having this issue when loading a cached redux state. If I just load the cached state, I'd run into errors for new app version with an updated state structure.

It was already mentioned, that lodash offers the merge function, which I used:

const currentInitialState = configureState().getState();
const mergedState = _.merge({}, currentInitialState, cachedState);
const store = configureState(mergedState);
Oporto answered 10/1, 2018 at 10:57 Comment(0)
O
3

New Method | Updated Answer

As of node v17, there is structuredClone that according to reference:

creates a deep clone of a given value using the structured clone algorithm.

So, we can use it like this to merge 2 objects:

const deepMerge = (obj1, obj2) => {
  const clone1 = structuredClone(obj1);
  const clone2 = structuredClone(obj2);

  for (let key in clone2) {
    if (clone2[key] instanceof Object && clone1[key] instanceof Object) {
      clone1[key] = deepMerge(clone1[key], clone2[key]);
    } else {
      clone1[key] = clone2[key];
    }
  }

  return clone1;
};


const first = { a: { x: 'x', y: 'y' }, b: 1 };
const second = { a: { x: 'xx' }, c: 2 };

const result = deepMerge(first, second);

console.log(result); // { a: { x: 'xx', y: 'y' }, b: 1, c: 2 }
Organotherapy answered 16/5, 2023 at 0:39 Comment(0)
E
2

Here's another one I just wrote that supports arrays. It concats them.

function isObject(obj) {
    return obj !== null && typeof obj === 'object';
}


function isPlainObject(obj) {
    return isObject(obj) && (
        obj.constructor === Object  // obj = {}
        || obj.constructor === undefined // obj = Object.create(null)
    );
}

function mergeDeep(target, ...sources) {
    if (!sources.length) return target;
    const source = sources.shift();

    if(Array.isArray(target)) {
        if(Array.isArray(source)) {
            target.push(...source);
        } else {
            target.push(source);
        }
    } else if(isPlainObject(target)) {
        if(isPlainObject(source)) {
            for(let key of Object.keys(source)) {
                if(!target[key]) {
                    target[key] = source[key];
                } else {
                    mergeDeep(target[key], source[key]);
                }
            }
        } else {
            throw new Error(`Cannot merge object with non-object`);
        }
    } else {
        target = source;
    }

    return mergeDeep(target, ...sources);
};
Encarnacion answered 30/5, 2017 at 19:11 Comment(0)
S
2

Use this function:

merge(target, source, mutable = false) {
        const newObj = typeof target == 'object' ? (mutable ? target : Object.assign({}, target)) : {};
        for (const prop in source) {
            if (target[prop] == null || typeof target[prop] === 'undefined') {
                newObj[prop] = source[prop];
            } else if (Array.isArray(target[prop])) {
                newObj[prop] = source[prop] || target[prop];
            } else if (target[prop] instanceof RegExp) {
                newObj[prop] = source[prop] || target[prop];
            } else {
                newObj[prop] = typeof source[prop] === 'object' ? this.merge(target[prop], source[prop]) : source[prop];
            }
        }
        return newObj;
    }
Spears answered 27/2, 2019 at 11:31 Comment(0)
P
2
// copies all properties from source object to dest object recursively
export function recursivelyMoveProperties(source, dest) {
  for (const prop in source) {
    if (!source.hasOwnProperty(prop)) {
      continue;
    }

    if (source[prop] === null) {
      // property is null
      dest[prop] = source[prop];
      continue;
    }

    if (typeof source[prop] === 'object') {
      // if property is object let's dive into in
      if (Array.isArray(source[prop])) {
        dest[prop] = [];
      } else {
        if (!dest.hasOwnProperty(prop)
        || typeof dest[prop] !== 'object'
        || dest[prop] === null || Array.isArray(dest[prop])
        || !Object.keys(dest[prop]).length) {
          dest[prop] = {};
        }
      }
      recursivelyMoveProperties(source[prop], dest[prop]);
      continue;
    }

    // property is simple type: string, number, e.t.c
    dest[prop] = source[prop];
  }
  return dest;
}

Unit test:

describe('recursivelyMoveProperties', () => {
    it('should copy properties correctly', () => {
      const source: any = {
        propS1: 'str1',
        propS2: 'str2',
        propN1: 1,
        propN2: 2,
        propA1: [1, 2, 3],
        propA2: [],
        propB1: true,
        propB2: false,
        propU1: null,
        propU2: null,
        propD1: undefined,
        propD2: undefined,
        propO1: {
          subS1: 'sub11',
          subS2: 'sub12',
          subN1: 11,
          subN2: 12,
          subA1: [11, 12, 13],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
        propO2: {
          subS1: 'sub21',
          subS2: 'sub22',
          subN1: 21,
          subN2: 22,
          subA1: [21, 22, 23],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
      };
      let dest: any = {
        propS2: 'str2',
        propS3: 'str3',
        propN2: -2,
        propN3: 3,
        propA2: [2, 2],
        propA3: [3, 2, 1],
        propB2: true,
        propB3: false,
        propU2: 'not null',
        propU3: null,
        propD2: 'defined',
        propD3: undefined,
        propO2: {
          subS2: 'inv22',
          subS3: 'sub23',
          subN2: -22,
          subN3: 23,
          subA2: [5, 5, 5],
          subA3: [31, 32, 33],
          subB2: false,
          subB3: true,
          subU2: 'not null --- ',
          subU3: null,
          subD2: ' not undefined ----',
          subD3: undefined,
        },
        propO3: {
          subS1: 'sub31',
          subS2: 'sub32',
          subN1: 31,
          subN2: 32,
          subA1: [31, 32, 33],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
      };
      dest = recursivelyMoveProperties(source, dest);

      expect(dest).toEqual({
        propS1: 'str1',
        propS2: 'str2',
        propS3: 'str3',
        propN1: 1,
        propN2: 2,
        propN3: 3,
        propA1: [1, 2, 3],
        propA2: [],
        propA3: [3, 2, 1],
        propB1: true,
        propB2: false,
        propB3: false,
        propU1: null,
        propU2: null,
        propU3: null,
        propD1: undefined,
        propD2: undefined,
        propD3: undefined,
        propO1: {
          subS1: 'sub11',
          subS2: 'sub12',
          subN1: 11,
          subN2: 12,
          subA1: [11, 12, 13],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
        propO2: {
          subS1: 'sub21',
          subS2: 'sub22',
          subS3: 'sub23',
          subN1: 21,
          subN2: 22,
          subN3: 23,
          subA1: [21, 22, 23],
          subA2: [],
          subA3: [31, 32, 33],
          subB1: false,
          subB2: true,
          subB3: true,
          subU1: null,
          subU2: null,
          subU3: null,
          subD1: undefined,
          subD2: undefined,
          subD3: undefined,
        },
        propO3: {
          subS1: 'sub31',
          subS2: 'sub32',
          subN1: 31,
          subN2: 32,
          subA1: [31, 32, 33],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
      });
    });
  });
Phenothiazine answered 11/4, 2019 at 13:30 Comment(0)
C
2

This is a cheap deep merge that uses as little code as I could think of. Each source overwrites the previous property when it exists.

const { keys } = Object;

const isObject = a => typeof a === "object" && !Array.isArray(a);
const merge = (a, b) =>
  isObject(a) && isObject(b)
    ? deepMerge(a, b)
    : isObject(a) && !isObject(b)
    ? a
    : b;

const coalesceByKey = source => (acc, key) =>
  (acc[key] && source[key]
    ? (acc[key] = merge(acc[key], source[key]))
    : (acc[key] = source[key])) && acc;

/**
 * Merge all sources into the target
 * overwriting primitive values in the the accumulated target as we go (if they already exist)
 * @param {*} target
 * @param  {...any} sources
 */
const deepMerge = (target, ...sources) =>
  sources.reduce(
    (acc, source) => keys(source).reduce(coalesceByKey(source), acc),
    target
  );

console.log(deepMerge({ a: 1 }, { a: 2 }));
console.log(deepMerge({ a: 1 }, { a: { b: 2 } }));
console.log(deepMerge({ a: { b: 2 } }, { a: 1 }));
Clydesdale answered 17/4, 2019 at 22:24 Comment(0)
C
2

Another variation using recursion, hope you find it useful.

const merge = (obj1, obj2) => {

    const recursiveMerge = (obj, entries) => {
         for (const [key, value] of entries) {
            if (typeof value === "object") {
               obj[key] = obj[key] ? {...obj[key]} : {};
               recursiveMerge(obj[key], Object.entries(value))
            else {
               obj[key] = value;
            }
          }

          return obj;
    }

    return recursiveMerge(obj1, Object.entries(obj2))
}
Calondra answered 25/11, 2020 at 9:2 Comment(0)
I
2

My use case for this was to merge default values into a configuration. If my component accepts a configuration object that has a deeply nested structure, and my component defines a default configuration, I wanted to set default values in my configuration for all configuration options that were not supplied.

Example usage:

export default MyComponent = ({config}) => {
  const mergedConfig = mergeDefaults(config, {header:{margins:{left:10, top: 10}}});
  // Component code here
}

This allows me to pass an empty or null config, or a partial config and have all of the values that are not configured fall back to their default values.

My implementation of mergeDefaults looks like this:

export default function mergeDefaults(config, defaults) {
  if (config === null || config === undefined) return defaults;
  for (var attrname in defaults) {
    if (defaults[attrname].constructor === Object) config[attrname] = mergeDefaults(config[attrname], defaults[attrname]);
    else if (config[attrname] === undefined) config[attrname] = defaults[attrname];
  }
  return config;
}


And these are my unit tests

import '@testing-library/jest-dom/extend-expect';
import mergeDefaults from './mergeDefaults';

describe('mergeDefaults', () => {
  it('should create configuration', () => {
    const config = mergeDefaults(null, { a: 10, b: { c: 'default1', d: 'default2' } });
    expect(config.a).toStrictEqual(10);
    expect(config.b.c).toStrictEqual('default1');
    expect(config.b.d).toStrictEqual('default2');
  });
  it('should fill configuration', () => {
    const config = mergeDefaults({}, { a: 10, b: { c: 'default1', d: 'default2' } });
    expect(config.a).toStrictEqual(10);
    expect(config.b.c).toStrictEqual('default1');
    expect(config.b.d).toStrictEqual('default2');
  });
  it('should not overwrite configuration', () => {
    const config = mergeDefaults({ a: 12, b: { c: 'config1', d: 'config2' } }, { a: 10, b: { c: 'default1', d: 'default2' } });
    expect(config.a).toStrictEqual(12);
    expect(config.b.c).toStrictEqual('config1');
    expect(config.b.d).toStrictEqual('config2');
  });
  it('should merge configuration', () => {
    const config = mergeDefaults({ a: 12, b: { d: 'config2' } }, { a: 10, b: { c: 'default1', d: 'default2' }, e: 15 });
    expect(config.a).toStrictEqual(12);
    expect(config.b.c).toStrictEqual('default1');
    expect(config.b.d).toStrictEqual('config2');
    expect(config.e).toStrictEqual(15);
  });
});

Instrument answered 28/11, 2020 at 13:48 Comment(0)
D
1

Sometimes you don't need deep merge, even if you think so. For example, if you have a default config with nested objects and you want to extend it deeply with your own config, you can create a class for that. The concept is very simple:

function AjaxConfig(config) {

  // Default values + config

  Object.assign(this, {
    method: 'POST',
    contentType: 'text/plain'
  }, config);

  // Default values in nested objects

  this.headers = Object.assign({}, this.headers, { 
    'X-Requested-With': 'custom'
  });
}

// Define your config

var config = {
  url: 'https://google.com',
  headers: {
    'x-client-data': 'CI22yQEI'
  }
};

// Extend the default values with your own
var fullMergedConfig = new AjaxConfig(config);

// View in DevTools
console.log(fullMergedConfig);

You can convert it to a function (not a constructor).

Dinka answered 28/5, 2017 at 1:36 Comment(0)
M
1
function isObject(obj) {
    return obj !== null && typeof obj === 'object';
}
const isArray = Array.isArray;

function isPlainObject(obj) {
    return isObject(obj) && (
        obj.constructor === Object  // obj = {}
        || obj.constructor === undefined // obj = Object.create(null)
    );
}

function mergeDeep(target, ...sources){
    if (!sources.length) return target;
    const source = sources.shift();

    if (isPlainObject(source) || isArray(source)) {
        for (const key in source) {
            if (isPlainObject(source[key]) || isArray(source[key])) {
                if (isPlainObject(source[key]) && !isPlainObject(target[key])) {
                    target[key] = {};
                }else if (isArray(source[key]) && !isArray(target[key])) {
                    target[key] = [];
                }
                mergeDeep(target[key], source[key]);
            } else if (source[key] !== undefined && source[key] !== '') {
                target[key] = source[key];
            }
        }
    }

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

// test...
var source = {b:333};
var source2 = {c:32, arr: [33,11]}
var n = mergeDeep({a:33}, source, source2);
source2.arr[1] = 22;
console.log(n.arr); // out: [33, 11]
Merwyn answered 20/7, 2017 at 8:32 Comment(1)
return mergeDeep(target, ...sources); will cause some recursion? Or I am missing something? Or is it modifying the original values?Dynamometer
C
1

There are well maintained libraries that already do this. One example on the npm registry is merge-deep

Commonality answered 10/3, 2018 at 3:55 Comment(0)
A
1

Use case: merging default configs

If we define configs in the form of:

const defaultConf = {
    prop1: 'config1',
    prop2: 'config2'
}

we can define more specific configs by doing:

const moreSpecificConf = {
    ...defaultConf,
    prop3: 'config3'
}

But if these configs contain nested structures this approach doesn't work anymore.

Therefore I wrote a function that only merges objects in the sense of { key: value, ... } and replaces the rest.

const isObject = (val) => val === Object(val);

const merge = (...objects) =>
    objects.reduce(
        (obj1, obj2) => ({
            ...obj1,
            ...obj2,
            ...Object.keys(obj2)
                .filter((key) => key in obj1 && isObject(obj1[key]) && isObject(obj2[key]))
                .map((key) => ({[key]: merge(obj1[key], obj2[key])}))
                .reduce((n1, n2) => ({...n1, ...n2}), {})
        }),
        {}
    );
Antiperiodic answered 12/11, 2019 at 12:4 Comment(0)
F
1

I am using the following short function for deep merging objects.
It works great for me.
The author completely explains how it works here.

/*!
 * Merge two or more objects together.
 * (c) 2017 Chris Ferdinandi, MIT License, https://gomakethings.com
 * @param   {Boolean}  deep     If true, do a deep (or recursive) merge [optional]
 * @param   {Object}   objects  The objects to merge together
 * @returns {Object}            Merged values of defaults and options
 * 
 * Use the function as follows:
 * let shallowMerge = extend(obj1, obj2);
 * let deepMerge = extend(true, obj1, obj2)
 */

var extend = function () {

    // Variables
    var extended = {};
    var deep = false;
    var i = 0;

    // Check if a deep merge
    if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) {
        deep = arguments[0];
        i++;
    }

    // Merge the object into the extended object
    var merge = function (obj) {
        for (var prop in obj) {
            if (obj.hasOwnProperty(prop)) {
                // If property is an object, merge properties
                if (deep && Object.prototype.toString.call(obj[prop]) === '[object Object]') {
                    extended[prop] = extend(extended[prop], obj[prop]);
                } else {
                    extended[prop] = obj[prop];
                }
            }
        }
    };

    // Loop through each object and conduct a merge
    for (; i < arguments.length; i++) {
        merge(arguments[i]);
    }

    return extended;

};
Footie answered 8/12, 2019 at 19:28 Comment(4)
While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - From ReviewSarazen
Hi @ChrisCamaratta. Not only is the essential part here, it's all here - the function and how to use it. So this is definitely not a link only answer. This is the function I have been using to deep merge objects. The link is only if you want the authors explanation of how it works. I feel it would be a disservice to the community to try and explain the workings better than the author who teaches JavaScript. Thanks for the comment.Footie
Huh. Either i missed it or the code did not appear in the reviewer interface when I reviewed it. I agree this is a quality answer. It would appear other reviewers overrode my initial assessment so i think you're ok. Sorry for the inspiration flag.Sarazen
Great! @ChrisCamaratta, Thanks for helping me understand what happened.Footie
F
1

https://lodash.com/docs/4.17.15#defaultsDeep

Note: This method mutates source.

_.defaultsDeep({ 'a': { 'b': 2 } }, { 'a': { 'b': 1, 'c': 3 } });
// => { 'a': { 'b': 2, 'c': 3 } }
Finochio answered 25/9, 2020 at 3:8 Comment(0)
C
1

Simple recursive solution

Using Object.entries, iterating over one of the objects. Adding the entry if it doesn't exist, and recursing if the entry is an object.

const x = { a: { a: 1 } }
const y = { a: { b: 1 } }

const z = JSON.parse(JSON.stringify(y))

const mergeIntoZ = (firstObj, secondObj) => {
  Object.entries(firstObj)
    .forEach(([key, value]) => {
      if (secondObj[key] === undefined) {
        secondObj[key] = value
      } else if (typeof value === 'object') {
        mergeIntoZ(firstObj[key], secondObj[key])
      }
    })

}
mergeIntoZ(x, z)
console.log(z)
Cavicorn answered 24/8, 2021 at 20:28 Comment(0)
T
1

Vanilla Script solution suitable for objects and arrays alike:

const x = { a: { a: 1 } }
const y = { a: { b: 1 } }
const z = { ...x, ...y } // { a: { b: 1 } }

function deepmerge() {
  merge = function () {
    let target = arguments[0];
    for (let i = 1; i < arguments.length ; i++) {
      let arr = arguments[i];
            for (let k in arr) {
         if (Array.isArray(arr[k])) {
            if (target[k] === undefined) {            
                 target[k] = [];
            }            
            target[k] = [...new Set(target[k].concat(...arr[k]))];
         } else if (typeof arr[k] === 'object') {
            if (target[k] === undefined) {            
                 target[k] = {};
            }
            target[k] = merge(target[k], arr[k]);
         } else {
              target[k] = arr[k];         
         }
      }
    }
    return target;
  }
  return merge(...arguments);
}
console.log(deepmerge(x,y));

Output:

{
  a: {
    a: 1,
    b: 1
  }
}
Triumvirate answered 19/2, 2022 at 17:26 Comment(0)
T
1

Simple, dependency-free, immutable (returns new object) deepMerge.

Does not try to be smart on non-object fields, b[key] overwrites a[key].

Visits each key exactly once.

Utilizes structuredClone.

function deepMerge(a, b) {
  const result = {};
  for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) {
    result[key] =
      a[key]?.constructor === Object && b[key]?.constructor === Object
        ? deepMerge(a[key], b[key])
        : structuredClone(b[key] !== undefined ? b[key] : a[key]);
  }
  return result;
}

The answered 31/7, 2023 at 9:56 Comment(0)
V
0

Does anybody know if deep merging exists in the ES6/ES7 spec?

Object.assign documentation suggests it doesn't do deep clone.

Viera answered 11/8, 2016 at 23:38 Comment(0)
E
0

I tried to write an Object.assignDeep which is based on the pollyfill of Object.assign on mdn.

(ES5)

Object.assignDeep = function (target, varArgs) { // .length of function is 2
    'use strict';
    if (target == null) { // TypeError if undefined or null
        throw new TypeError('Cannot convert undefined or null to object');
    }

    var to = Object(target);

    for (var index = 1; index < arguments.length; index++) {
        var nextSource = arguments[index];

        if (nextSource != null) { // Skip over if undefined or null
            for (var nextKey in nextSource) {
                // Avoid bugs when hasOwnProperty is shadowed
                if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
                    if (typeof to[nextKey] === 'object' 
                        && to[nextKey] 
                        && typeof nextSource[nextKey] === 'object' 
                        && nextSource[nextKey]) {                        
                        Object.assignDeep(to[nextKey], nextSource[nextKey]);
                    } else {
                        to[nextKey] = nextSource[nextKey];
                    }
                }
            }
        }
    }
    return to;
};
console.log(Object.assignDeep({},{a:{b:{c:1,d:1}}},{a:{b:{c:2,e:2}}}))
Elevenses answered 7/3, 2017 at 11:1 Comment(0)
S
0

I found only 2 line solution to get deep merge in javascript. Do let me know how this works out for you.

const obj1 = { a: { b: "c", x: "y" } }
const obj2 = { a: { b: "d", e: "f" } }
temp = Object.assign({}, obj1, obj2)
Object.keys(temp).forEach(key => {
    temp[key] = (typeof temp[key] === 'object') ? Object.assign(temp[key], obj1[key], obj2[key]) : temp[key])
}
console.log(temp)

Temp object will print { a: { b: 'd', e: 'f', x: 'y' } }

Spriggs answered 11/12, 2019 at 9:4 Comment(4)
This doesn't do actual deep merge. It will fail with merge({x:{y:{z:1}}}, {x:{y:{w:2}}}). Il will also fail to update existing values in obj1 if obj2 has them too, for example with merge({x:{y:1}}, {x:{y:2}}).Lyricist
A slight modification in the above function will do the actual deep merge const deepMerge = (ob1, ob2) => { const temp = Object.assign({}, ob1, ob2); Object.keys(temp).forEach((key) => { if (typeof temp[key] === "object") { temp[key] = deepMerge(ob1[key], ob2[key]); } }); return temp; };Darell
Recursive calling will not work as temp will be reassigned and you will not be able to set result back.Spriggs
are you checking if for more the two-level deep?Outdated
B
0

If you want to merge multiple plain objects (do not modify input objects). Based on Object.assign polyfill

function isPlainObject(a) {
    return (!!a) && (a.constructor === Object);
}

function merge(target) {
    let to = Object.assign({}, target);

    for (let index = 1; index < arguments.length; index++) {
        let nextSource = arguments[index];

        if (nextSource !== null && nextSource !== undefined) {
            for (let nextKey in nextSource) {
                // Avoid bugs when hasOwnProperty is shadowed
                if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
                    if (isPlainObject(to[nextKey]) && isPlainObject(nextSource[nextKey])) {
                        to[nextKey] = merge(to[nextKey], nextSource[nextKey]);
                    } else {
                        to[nextKey] = nextSource[nextKey];
                    }
                }
            }
        }
    }

    return to;
}

// Usage

var obj1 = {
    a: 1,
    b: {
        x: 2,
        y: {
            t: 3,
            u: 4
        }
    },
    c: "hi"
};

var obj2 = {
    b: {
        x: 200,
        y: {
            u: 4000,
            v: 5000
        }
    }
};

var obj3 = {
    c: "hello"
};

console.log("result", merge(obj1, obj2, obj3));
console.log("obj1", obj1);
console.log("obj2", obj2);
console.log("obj3", obj3);

If you want to merge with limited depth

function isPlainObject(a) {
        return (!!a) && (a.constructor === Object);
    }

function merge(target) {
let to = Object.assign({}, target);

const hasDepth = arguments.length > 2 && typeof arguments[arguments.length - 1] === 'number';

const depth = hasDepth ? arguments[arguments.length - 1] : Infinity;

const lastObjectIndex = hasDepth ? arguments.length - 2 : arguments.length - 1;

for (let index = 1; index <= lastObjectIndex; index++) {
    let nextSource = arguments[index];

    if (nextSource !== null && nextSource !== undefined) {
        for (let nextKey in nextSource) {
            // Avoid bugs when hasOwnProperty is shadowed
            if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
                if (depth > 0 && isPlainObject(to[nextKey]) && isPlainObject(nextSource[nextKey])) {
                    to[nextKey] = merge(to[nextKey], nextSource[nextKey], depth - 1);
                } else {
                    to[nextKey] = nextSource[nextKey];
                }
            }
        }
    }
}

return to;
}

// Usage

var obj1 = {
    a: 1,
    b: {
        x: 2,
        y: {
            t: 3,
            u: 4,
            z: {zzz: 100}
        }
    },
    c: "hi"
};

var obj2 = {
    b: {
        y: {
            u: 4000,
            v: 5000,
            z: {}
        }
    }
};

var obj3 = {
    c: "hello"
};

console.log('deep 0', merge(obj1, obj2, obj3, 0));
console.log('deep 1', merge(obj1, obj2, obj3, 1));
console.log('deep 2', merge(obj1, obj2, obj3, 2));
console.log('deep 2', merge(obj1, obj2, obj3, 4));
Belfort answered 15/8, 2021 at 7:9 Comment(2)
Its not working when two or more objects has array data type, array is overridden.Pellegrini
@Pellegrini we should merge plain objects only as we do not have a clear rule to merge other types. You can refer deepmerge of MUI: github.com/mui-org/material-ui/blob/next/packages/mui-utils/src/…Superstructure
A
0

I've gone through all of the answers here and pieced together one of my own. Most of the existing answers didn't work the way I wanted.

This is pretty horrid for 2021 so any tips to improve, I'm all ears!

This is in Typescript

type Props = Record<string, any>

export const deepMerge = (target: Props, ...sources: Props[]): Props => {
  if (!sources.length) {
    return target
  }

  Object.entries(sources.shift() ?? []).forEach(([key, value]) => {
    if (!target[key]) {
      Object.assign(target, { [key]: {} })
    }

    if (
      value.constructor === Object ||
      (value.constructor === Array && value.find(v => v.constructor === Object))
    ) {
      deepMerge(target[key], value)
    } else if (value.constructor === Array) {
      Object.assign(target, {
        [key]: value.find(v => v.constructor === Array)
          ? target[key].concat(value)
          : [...new Set([...target[key], ...value])],
      })
    } else {
      Object.assign(target, { [key]: value })
    }
  })

  return target
}

Flat arrays get duplicate values removed using [...new Set(...)].

Nested arrays are joined using concat.

Abad answered 22/12, 2021 at 16:38 Comment(0)
W
0

(native solution) If you know the properties you want to deep merge, then

const x = { a: { a: 1 } }
const y = { a: { b: 1 } }
Object.assign(y.a, x.a);
Object.assign(x, y);
// output: a: {b: 1, a: 1}
Winterwinterbottom answered 27/5, 2022 at 8:56 Comment(0)
C
0

I have written a simpler deep merge without using any 3rd party library.

function merge(object1, object2) {
    /* Start iterating over each key of the object. */
    for (const key in object2) {
        /* 1). When object1 has the same key as object2. */
        if (object1[key]) {
            /* 1.1). When both values are type of object then again recursively call merge on those inner objects. */
            if(typeof object1[key] === "object" && typeof object2[key] === "object")
                object1[key] = merge(object1[key], object2[key]);
            /* 1.1). When both values are some other type then update the value in object1 from object2. */
            else
                object1[key] = object2[key];            
        } else {
            /* 2). When object1 doesn't have the same key as object2. */
            if(typeof object2[key] === "object")
                /* 2.1). If the value is of type object, then copy the entire value into object1. */
                Object.assign(object1, { [key]: object2[key] });
            else
                /* 2.2). If both objects are totally different then copy all keys from object2 to object1. */
                Object.assign(object1, object2);
        }
    }
    return object1;
}
const object1 = { a: { a:1 } };
const object2 = { a: { b:1 } };
console.log(merge(object1, object2));

Since we are merging object2 into object1, if same key is found in both the objects with primitive values, then it will update the key from object2 into object1.

Commit answered 2/11, 2023 at 11:45 Comment(0)
D
-1

I make this method for deep assign using es6.

function isObject(item) {
  return (item && typeof item === 'object' && !Array.isArray(item) && item !== null)
}

function deepAssign(...objs) {
    if (objs.length < 2) {
        throw new Error('Need two or more objects to merge')
    }

    const target = objs[0]
    for (let i = 1; i < objs.length; i++) {
        const source = objs[i]
        Object.keys(source).forEach(prop => {
            const value = source[prop]
            if (isObject(value)) {
                if (target.hasOwnProperty(prop) && isObject(target[prop])) {
                    target[prop] = deepAssign(target[prop], value)
                } else {
                    target[prop] = value
                }
            } else if (Array.isArray(value)) {
                if (target.hasOwnProperty(prop) && Array.isArray(target[prop])) {
                    const targetArray = target[prop]
                    value.forEach((sourceItem, itemIndex) => {
                        if (itemIndex < targetArray.length) {
                            const targetItem = targetArray[itemIndex]

                            if (Object.is(targetItem, sourceItem)) {
                                return
                            }

                            if (isObject(targetItem) && isObject(sourceItem)) {
                                targetArray[itemIndex] = deepAssign(targetItem, sourceItem)
                            } else if (Array.isArray(targetItem) && Array.isArray(sourceItem)) {
                                targetArray[itemIndex] = deepAssign(targetItem, sourceItem)
                            } else {
                                targetArray[itemIndex] = sourceItem
                            }
                        } else {
                            targetArray.push(sourceItem)
                        }
                    })
                } else {
                    target[prop] = value
                }
            } else {
                target[prop] = value
            }
        })
    }

    return target
}
Deferred answered 8/8, 2016 at 5:47 Comment(0)
U
-2

It doesn't exist but you can use JSON.parse(JSON.stringify(jobs))

Unsung answered 6/3, 2017 at 12:55 Comment(3)
That will deep-copy / clone an object, but not merge two objects.Colwell
This won't copy methods from one object to another because JSON does not support the function typeLonni
If function cloning is not an issue, you could do something like.... Object.assign({ first: 'object'}, JSON.parse(JSON.stringify({ second: 'object'})));Cyclamate
C
-2

There is a lodash package which specifically deals only with deep cloning a object. The advantage is that you don't have to include the entire lodash library.

Its called lodash.clonedeep

In nodejs the usage is like this

var cloneDeep = require('lodash.clonedeep');
 
const newObject = cloneDeep(oldObject);

In ReactJS the usage is

import cloneDeep from 'lodash/cloneDeep';

const newObject = cloneDeep(oldObject);

Check the docs here . If you are interested in how it works take a look at the source file here

Capsule answered 21/7, 2020 at 4:49 Comment(1)
The question was about merging, not cloning.Diocletian
M
-4

This is simple and works:

let item = {
    firstName: 'Jonnie',
    lastName: 'Walker',
    fullName: function fullName() {
            return 'Jonnie Walker';
    }
Object.assign(Object.create(item), item);

Explain:

Object.create() Creates new Object. If you pass params to function it will creates you object with prototype of other object. So if you have any functions on prototype of object they will be passed to prototype of other object.

Object.assign() Merges two objects and creates fully new object and they have no reference anymore. So this example works good for me.

Monthly answered 2/12, 2016 at 11:26 Comment(2)
It's not copy getter/setter functions.Anthracoid
how does this "go deep" ?Metamorphic

© 2022 - 2024 — McMap. All rights reserved.