JavaScript Deep Merge

I recently shared how you can merge object properties with the spread operator but this method has one big limitation:  the spread operator merge isn’t a “deep” merge, meaning merges are recursive.  Moreover nested object properties aren’t merged — the last value specified in the merge replaces the last, even when there are other properties that should exist.

const defaultPerson = { name: 'Anon', gender: 'Female', hair: { color: 'brown', cut: 'long' }, eyes: 'blue', family: ['mom', 'dad']
}; const me = { name: 'David Walsh', gender: 'Male', hair: { cut: 'short' }, family: ['wife', 'kids', 'dog']
}; const summary = {...defaultPerson, ...me}; /*
{ "name":"David Walsh", "gender":"Male", "hair":{ "cut":"short" }, "eyes":"blue", "family":[ "wife", "kids", "dog" ]
}
*/

In the sample above, you’ll notice that the hair object’s color is gone instead of merged because the spread operator simply keeps the last provided values, which in this case is me.hair.  The same merge problem applies to arrays — you’ll notice mom and dad aren’t merged from the defaultPerson object’s family array.  Yikes!

Deep merging in JavaScript is important, especially with the common practice of “default” or “options” objects with many properties and nested objects that often get merged with instance-specific values.  If you’re looking for a utility to help with deep merges, look no further than the tiny deepmerge utility!

When you use the deepmerge utility, you can recursively merge any number of objects (including arrays) into one final object.  Let’s take a look!

const deepmerge = require('deepmerge'); // ... const summary = deepmerge(defaultPerson, me); /*
{ "name":"David Walsh", "gender":"Male", "hair":{ "color":"brown", "cut":"short" }, "eyes":"blue", "family":[ "mom", "dad", "wife", "kids", "dog" ]
}
*/

deepmerge can handle much more complicated merges: nested objects and deepmerge.all to merge more than two objects:

const result = deepmerge.all([, { level1: { level2: { name: 'David', parts: ['head', 'shoulders'] } } }, { level1: { level2: { face: 'meh', parts: ['knees', 'toes'] } } }, { level1: { level2: { eyes: 'more meh', parts: ['eyes'] } } },
]); /*
{ "level1":{ "level2":{ "name":"David", "parts":[ "head", "shoulders", "knees", "toes", "eyes" ], "face":"meh", "eyes":"more meh" } }
}
*/

deepmerge is an amazing utility is a relatively small amount of code:

function isMergeableObject(val) { var nonNullObject = val && typeof val === 'object' return nonNullObject && Object.prototype.toString.call(val) !== '[object RegExp]' && Object.prototype.toString.call(val) !== '[object Date]'
} function emptyTarget(val) { return Array.isArray(val) ? [] : {}
} function cloneIfNecessary(value, optionsArgument) { var clone = optionsArgument && optionsArgument.clone === true return (clone && isMergeableObject(value)) ? deepmerge(emptyTarget(value), value, optionsArgument) : value
} function defaultArrayMerge(target, source, optionsArgument) { var destination = target.slice() source.forEach(function(e, i) { if (typeof destination[i] === 'undefined') { destination[i] = cloneIfNecessary(e, optionsArgument) } else if (isMergeableObject(e)) { destination[i] = deepmerge(target[i], e, optionsArgument) } else if (target.indexOf(e) === -1) { destination.push(cloneIfNecessary(e, optionsArgument)) } }) return destination
} function mergeObject(target, source, optionsArgument) { var destination = {} if (isMergeableObject(target)) { Object.keys(target).forEach(function (key) { destination[key] = cloneIfNecessary(target[key], optionsArgument) }) } Object.keys(source).forEach(function (key) { if (!isMergeableObject(source[key]) || !target[key]) { destination[key] = cloneIfNecessary(source[key], optionsArgument) } else { destination[key] = deepmerge(target[key], source[key], optionsArgument) } }) return destination
} function deepmerge(target, source, optionsArgument) { var array = Array.isArray(source); var options = optionsArgument || { arrayMerge: defaultArrayMerge } var arrayMerge = options.arrayMerge || defaultArrayMerge if (array) { return Array.isArray(target) ? arrayMerge(target, source, optionsArgument) : cloneIfNecessary(source, optionsArgument) } else { return mergeObject(target, source, optionsArgument) }
} deepmerge.all = function deepmergeAll(array, optionsArgument) { if (!Array.isArray(array) || array.length 

Little code with big functionality?  That's my favorite type of utility!  deepmerge is used all over the web and for good reason!

Related Post

Rojenx is a leading concept artist who work appears in games and publications

Check out his personal gallery here

In other news …