Mike
Mike

Reputation: 7704

How to deep merge instead of shallow merge?

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?

Upvotes: 653

Views: 426810

Answers (30)

Teiem
Teiem

Reputation: 1629

This version merges objects (dictionaries) only, everything else gets overwritten by the supplied second object.

const isObject = (item) => item?.constructor === Object;
export const combine = (objA, objB) => Object.fromEntries([...new Set([...Object.keys(objA), ...Object.keys(objB)])].map(key => [ key, isObject(objA[key]) && isObject(objB[key]) ? combine(objA[key], objB[key]) : objB[key] ?? objA[key] ]));

Upvotes: 0

V. Sambor
V. Sambor

Reputation: 13389

Another way of handling the deep merge and the circular reference can be done using a map:

function mergeDeep(target, source, seen = new Map()) {
    const isObject = obj => obj && typeof obj === 'object';

    if (seen.has(source)) {
        return seen.get(source);
    }
    
    seen.set(source, target);

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

    return target;
}

const a = {
   "p1": 1,
   "arr1": [4, 5, 6],
   "obj": {
     "a1": {
       "aa1": 'test'
     },
     "b1": 2
   }
};

const b = {
   "p1": 5,
   "arr1": [7, 8, 9],
   "p2": 15,
   "obj": {
     "a1": {
       "ab": false
     },
     "b1": {
        "ba1": null
     }
   }
};

// Adding circular references
a.obj.circularRef = a;
b.obj.circularRef = b;

const c = mergeDeep(a, b);

console.log ('merged: ', c);

Upvotes: 0

Calculamatrise
Calculamatrise

Reputation: 419

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

Update

I have reworked a few things to prevent accidents.

Object.defineProperty(Object, 'merge', {
    value: function merge(target, ...sources) {
        if (typeof target == 'undefined') {
            throw new TypeError("Cannot convert undefined or null to object");
        } else if (Array.isArray(target)) {
            target.push(...sources.flat());
            return target;
        }
        for (const source of sources) {
            for (const key in source) {
                if (!source.hasOwnProperty(key)) continue;
                if (typeof target[key] == 'object' && target[key] !== null && typeof source[key] == 'object') {
                    Object.merge(target[key], source[key]);
                    continue;
                } else if (Array.isArray(target[key]) && Array.isArray(source[key])) {
                    target[key] = target[key].concat(source[key]);
                    continue;
                }

                target[key] = source[key];
            }
        }

        return target;
    },
    writable: true
});

To avoid messing with every object's prototype, I added the utility function as a method of Object. Follow the example for usage.

let object = {
    key: 'value',
    property: {
        subKey: 'subValue',
        subProperty: {
            subPropertyKey: 'subPropertyValue'
        }
    }
};

Object.merge(object, {
    key: 'newValue'
}, {
    property: {
        subKey: 'newValue'
    }
}, {
    property: {
        newSubKey: 'newSubValue',
        subProperty: {
            subPropertyKey: 'newSubPropertyValue'
        }
    }
});

As a result, the following object is produced.

{
    key: "newValue",
    property: {
        newSubKey: "newSubValue",
        subKey: "newValue",
        subProperty: {
            subPropertyKey: "newSubPropertyValue"
        }
    }
}

It is essentially Object.assign with more depth and security.

Upvotes: 7

John Hildenbiddle
John Hildenbiddle

Reputation: 3335

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);

Upvotes: 116

pery mimon
pery mimon

Reputation: 8315

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://stackoverflow.com/a/71177790/1919821

Upvotes: 20

varad_s
varad_s

Reputation: 1192

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.

Upvotes: 0

atmin
atmin

Reputation: 1489

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;
}

Upvotes: 1

MoKhajavi75
MoKhajavi75

Reputation: 2702

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 }

Upvotes: 5

AndrewHenderson
AndrewHenderson

Reputation: 4972

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>

Upvotes: 250

fredtma
fredtma

Reputation: 1053

(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}

Upvotes: 0

Timor Kodal
Timor Kodal

Reputation: 364

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

Upvotes: 1

devmatic
devmatic

Reputation: 37

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.

Upvotes: 0

Martin Braun
Martin Braun

Reputation: 12589

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.

Upvotes: 45

Nermin
Nermin

Reputation: 935

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)

Upvotes: 1

Văn Quyết
Văn Quyết

Reputation: 2526

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));

Upvotes: 0

priolo priolus
priolo priolus

Reputation: 372

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

Upvotes: 5

bikeman868
bikeman868

Reputation: 2637

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);
  });
});

Upvotes: 2

spedy
spedy

Reputation: 2360

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))
}

Upvotes: 2

hojin
hojin

Reputation: 1395

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

Upvotes: 1

kiranvj
kiranvj

Reputation: 34117

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

Upvotes: -2

saumilsdk
saumilsdk

Reputation: 847

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

Upvotes: 0

John Shearing
John Shearing

Reputation: 259

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;

};

Upvotes: 1

Vincent
Vincent

Reputation: 4753

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.

Upvotes: 19

Spenhouet
Spenhouet

Reputation: 7169

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}), {})
        }),
        {}
    );

Upvotes: 1

Sergey Gurin
Sergey Gurin

Reputation: 1563

// 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,
        },
      });
    });
  });

Upvotes: 2

afonte
afonte

Reputation: 988

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

Upvotes: 4

Lewis
Lewis

Reputation: 5879

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 }));

Upvotes: 2

Vikram Biwal
Vikram Biwal

Reputation: 2826

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;
    }

Upvotes: 2

Ezequiel
Ezequiel

Reputation: 406

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.

Upvotes: 6

user3336882
user3336882

Reputation: 3313

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

Upvotes: 12

Related Questions