Skip to the code
cf.

Deep merge

Write deepMerge(...objects). It merges any number of plain objects, left to right, into a new object, the way you'd layer a user's settings over the defaults.

When two objects both have a plain object under the same key, merge those too, recursively. In every other case the later value wins, whatever it is: arrays, dates, and class instances replace the earlier value rather than merging with it. A key whose value is undefined is skipped, so it never erases an earlier value.

Don't change any argument. Every plain object in the result is new, so changing the result can't reach the inputs; other values, arrays included, are used as they are. Ignore any key named __proto__, so merging parsed JSON can't change Object.prototype.

const defaults = { theme: { color: "blue", size: 12 }, plugins: ["a", "b"], debug: false };
const user = { theme: { size: 14 }, plugins: ["c"], debug: undefined };
 
deepMerge(defaults, user);
// { theme: { color: "blue", size: 14 }, plugins: ["c"], debug: false }

What the tests check

  1. merges nested objects
    expect(deepMerge({ a: { x: 1 } }, { a: { y: 2 } })).toEqual({ a: { x: 1, y: 2 } });
    
  2. lets later values win
    expect(deepMerge({ a: 1, b: { c: 1, d: 1 } }, { a: 2, b: { c: 2 } })).toEqual({ a: 2, b: { c: 2, d: 1 } });
    
  3. merges any number of objects, left to right
    expect(deepMerge({ a: 1 }, { b: { x: 1 } }, { b: { y: 2 } }, { a: 3 })).toEqual({ a: 3, b: { x: 1, y: 2 } });
    expect(deepMerge()).toEqual({});
    
  4. replaces arrays instead of merging them
    expect(deepMerge({ tags: ["a", "b"] }, { tags: ["c"] })).toEqual({ tags: ["c"] });
    expect(deepMerge({ list: { 0: "x" } }, { list: ["y"] })).toEqual({ list: ["y"] });
    
  5. lets any value replace an object, and an object replace any value
    expect(deepMerge({ a: { x: 1 } }, { a: null })).toEqual({ a: null });
    expect(deepMerge({ a: { x: 1 } }, { a: 5 })).toEqual({ a: 5 });
    expect(deepMerge({ a: 1 }, { a: { x: 1 } })).toEqual({ a: { x: 1 } });
    
  6. takes dates and class instances whole
    class Point {
      constructor() {
        this.x = 1;
      }
    }
    const when = new Date(0);
    const at = new Point();
    const result = deepMerge({ when: { y: 1 }, at: { y: 2 } }, { when, at });
    expect(result.when).toBe(when);
    expect(result.at).toBe(at);
    
  7. skips undefined values
    expect(deepMerge({ a: 1, b: { c: 1 } }, { a: undefined, b: { c: undefined } })).toEqual({ a: 1, b: { c: 1 } });
    expect(deepMerge({ a: 1 }, { z: undefined })).toEqual({ a: 1 });
    
  8. doesn't change the inputs or share objects with them
    const first = { a: { x: 1 } };
    const second = { b: { y: 2 } };
    const result = deepMerge(first, second);
    result.a.x = 99;
    result.b.y = 99;
    expect(first).toEqual({ a: { x: 1 } });
    expect(second).toEqual({ b: { y: 2 } });
    
  9. ignores __proto__ keys
    try {
      const result = deepMerge({}, JSON.parse('{ "__proto__": { "polluted": true }, "ok": 1 }'));
      expect(({}).polluted).toBeUndefined();
      expect(result.polluted).toBeUndefined();
      expect(result).toEqual({ ok: 1 });
    } finally {
      delete Object.prototype.polluted;
    }
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).