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.
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.
}
// 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.
Date, Map, RegExp, null, and any other non-plain-object as opaque values — the source one wins, no recursion inside.{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 not — null overwrites. They are different.target or source.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.