Skip to content

Instantly share code, notes, and snippets.

@danieldietrich
Last active September 25, 2022 21:10
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danieldietrich/0bd56068825a78321d532ce021d38edc to your computer and use it in GitHub Desktop.
Save danieldietrich/0bd56068825a78321d532ce021d38edc to your computer and use it in GitHub Desktop.
Deeply merging JavaScript objects

Deeply merging JavaScript objects

This method is like Object.assign except that it recursively merges own enumerable string keyed properties of source objects into the destination object. Source properties that resolve to undefined are skipped if a destination value exists. Arrays are concatenated. Plain object properties are merged recursively. Other objects and value types are overridden by assignment. Source objects are applied from left to right. Subsequent sources overwrite property assignments of previous sources.

Note: This method mutates target.

Motivation

  • The native Object.assign does simply overwrite values on duplicate keys.
  • The Lodash _.merge does not handle arrays the way I would expect it:
// Lodash returns [{}]
_.merge([{}], [{}])

// This returns [{}, {}]
merge([{}], [{}])

The order of objects within arrays does not necessarily matter. Lodash merges objects that have the same index within different arrays. This might lead to unexpected results if the objects are unrelated.

My real world use case was merging multiple OpenAPI specifications of several microservices into one big REST documentation for an API gateway.

Examples

const object = {
  a: [{b: 2}, {d: 4}]
};
 
const other = {
  a: [{c: 3}, {e: 5}]
};

// 1. Native JavaScript
Object.assign(object, other);
// = {a: [{c: 3}, {e: 5}]}

// 2. Lodash
_.merge(object, other);
// = {a: [{b: 2, c: 3}, {d: 4, e: 5}]}

// 3. This merge
merge(object, other);
// = {a: [{b: 2}, {d: 4}, {c: 3}, {e: 5}]}
// JavaScript version
function merge(target, ...sources) {
return sources.reduce(m, target);
function m(dst, src) {
for (const [key, value2] of Object.entries(src)) {
const value1 = dst[key];
dst[key] =
(Array.isArray(value1) && Array.isArray(value2)) ? value1.concat(value2) :
(value1 instanceof Object && value2 instanceof Object) ? m(value1, value2) :
(value1 === undefined || value2 !== undefined) ? value2 : value1;
}
return dst;
}
}
// TypeScript version
interface Obj {
[key: string]: any; // tslint:disable-line:no-any
}
function merge(target: Obj, ...sources: Obj[]): Obj {
return sources.reduce(m, target);
function m(dst: Obj, src: Obj): Obj {
for (const [key, value2] of Object.entries(src)) {
const value1 = dst[key];
dst[key] =
(Array.isArray(value1) && Array.isArray(value2)) ? value1.concat(value2) :
(value1 instanceof Object && value2 instanceof Object) ? m(value1, value2) :
(value1 === undefined || value2 !== undefined) ? value2 : value1;
}
return dst;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment