Deep Merge Plain Objects Safely
Merge nested configuration objects without mutation, cloning arrays and special values while blocking prototype-pollution keys.
Code
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
function isPlainObject(value) {
if (value === null || typeof value !== "object") return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}
function cloneValue(value) {
if (Array.isArray(value)) return value.map(cloneValue);
if (value instanceof Date) return new Date(value.getTime());
if (value instanceof Map) {
return new Map([...value].map(([key, item]) => [key, cloneValue(item)]));
}
if (value instanceof Set) {
return new Set([...value].map(cloneValue));
}
if (isPlainObject(value)) {
const output = {};
for (const [key, item] of Object.entries(value)) {
if (!DANGEROUS_KEYS.has(key)) output[key] = cloneValue(item);
}
return output;
}
return value;
}
function mergeInto(target, source) {
for (const [key, value] of Object.entries(source)) {
if (DANGEROUS_KEYS.has(key) || value === undefined) continue;
if (isPlainObject(value)) {
const base = isPlainObject(target[key]) ? target[key] : {};
target[key] = mergeInto({ ...base }, value);
} else {
target[key] = cloneValue(value);
}
}
return target;
}
export function deepMerge(...sources) {
const result = {};
for (const source of sources) {
if (source === undefined || source === null) continue;
if (!isPlainObject(source)) {
throw new TypeError("deepMerge only accepts plain objects");
}
mergeInto(result, source);
}
return result;
}
const defaults = {
server: { host: "localhost", port: 3000, tls: false },
tags: ["api"],
limits: { rpm: 60 },
};
const overrides = {
server: { port: 8080, tls: true },
tags: ["prod", "api"],
extra: null,
};
const merged = deepMerge(defaults, overrides, { server: { host: "0.0.0.0" } });
console.log(JSON.stringify(merged));
console.log(defaults.server.port, defaults.tags.join(","));
const polluted = deepMerge(
defaults,
JSON.parse('{"__proto__":{"polluted":true}}'),
);
console.log(polluted.polluted === undefined, {}.polluted === undefined);Report a problem with this exampleOpens a pre-filled issue on GitHub.