All questions

Deep Merge

Premium

Deep Merge

Implement deepMerge(target, source) that recursively combines two plain objects. When both sides have an object at the same key, you merge those nested objects too — instead of letting the right side overwrite the left. For primitives, arrays, and conflicting types, the right side (the source) wins.

This is the same idea as Object.assign or the spread operator ({...a, ...b}), but applied all the way down the object tree instead of only at the top level. The shallow versions are destructive at depth — {...defaults, ...overrides} will replace defaults.theme entirely if overrides.theme exists, even when the override only sets one nested field. Deep merge is the standard fix when layering config: defaults < environment < user preferences, where each layer should only override the keys it actually mentions.

Signature

function deepMerge(target, source) {
  // returns a NEW object — does not mutate target or source.
  // Plain objects at matching keys are merged recursively.
  // Everything else: source wins.
}

Examples

// Nested plain objects merge instead of overwriting.
deepMerge({ a: { b: 1 } }, { a: { c: 2 } });
// → { a: { b: 1, c: 2 } }
// Source wins on primitive conflicts and arrays.
deepMerge(
  { name: 'old', tags: [1, 2], meta: { v: 1 } },
  { name: 'new', tags: [3],    meta: { v: 2, x: 9 } },
);
// → { name: 'new', tags: [3], meta: { v: 2, x: 9 } }
//   tags is REPLACED (not concatenated); meta is merged recursively.

Notes

  • Plain objects only recurse. Treat arrays, Date, Map, RegExp, null, and any other non-plain-object as opaque values — the source one wins, no recursion inside.
  • Arrays are replaced, not concatenated. {a: [1, 2]} merged with {a: [3]} gives {a: [3]}. Concatenating is surprising — most production utilities (lodash merge, Ramda) also replace.
  • undefined on the source side keeps the target's value. Explicit null does notnull overwrites. They are different.
  • Do not mutate either input. Return a new object; nested objects should also be fresh, not shared references with target or source.
  • You can assume the inputs are not circular. A self-referencing object is out of scope and should throw via stack overflow — that's acceptable.
  • You only need to handle own enumerable string keys. Symbols and inherited properties are out of scope.

A typical use case: deepMerge({ db: { host: 'localhost', port: 5432 }, log: { level: 'info' } }, { db: { port: 6000 } }) should yield { db: { host: 'localhost', port: 6000 }, log: { level: 'info' } } — the override only touches db.port, and db.host and log.level survive.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium