Skip to content
GreenSnippets

Deep merge plain objects

Recursively merge configuration objects without mutating the inputs and without prototype pollution.

JavaScript
const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);

function isPlainObject(value) {
  return value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype;
}

function deepMerge(...sources) {
  const result = {};
  for (const source of sources) {
    if (!isPlainObject(source)) continue;
    for (const [key, value] of Object.entries(source)) {
      if (UNSAFE_KEYS.has(key)) continue;
      if (isPlainObject(value)) {
        result[key] = deepMerge(isPlainObject(result[key]) ? result[key] : {}, value);
      } else {
        result[key] = value;
      }
    }
  }
  return result;
}

Example / expected output

Output
deepMerge({ a: { x: 1, y: 2 } }, { a: { y: 3 }, b: 4 }) // { a: { x: 1, y: 3 }, b: 4 }

How it works

Spread ({...a, ...b}) only merges one level. This helper merges nested plain objects, lets later sources win, replaces arrays instead of concatenating them, and skips __proto__, constructor and prototype keys so untrusted input cannot pollute Object.prototype.