Skip to main content Accessibility Feedback

deepMerge.js

Deeply merge two or more objects or arrays.

Source Code

Example

let merlin = {
	job: 'Wizard',
	spells: ['Dancing teacups', 'Disappear'],
	pet: 'owl'
};

let radagast = {
	job: 'Druid',
	spells: ['Talk to animals', 'Navigate'],
	tool: 'staff',
	age: 179
};

let mergedWizards = deepMerge(merlin, radagast);

The helper function

/**
 * Deep merge two or more objects or arrays.
 * (c) Chris Ferdinandi, MIT License, https://gomakethings.com
 * @param   {*} ...objs  The arrays or objects to merge
 * @returns {*}          The merged arrays or objects
 */
function deepMerge (...objs) {

	/**
	 * Get the object type
	 * @param  {*}       obj The object
	 * @return {String}      The object type
	 */
	function getType (obj) {
		return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
	}

	/**
	 * Deep merge two objects
	 * @return {Object}
	 */
	function mergeObj (clone, obj) {
		for (let [key, value] of Object.entries(obj)) {
			let type = getType(value);
			if (clone[key] !== undefined && getType(clone[key]) === type && ['array', 'object'].includes(type)) {
				clone[key] = deepMerge(clone[key], value);
			} else {
				clone[key] = structuredClone(value);
			}
		}
	}

	// Create a clone of the first item in the objs array
	let clone = structuredClone(objs.shift());

	// Loop through each item
	for (let obj of objs) {

		// Get the object type
		let type = getType(obj);

		// If the current item isn't the same type as the clone, replace it
		if (getType(clone) !== type) {
			clone = structuredClone(obj);
			continue;
		}

		// Otherwise, merge
		if (type === 'array') {
			clone = [...clone, ...structuredClone(obj)];
		} else if (type === 'object') {
			mergeObj(clone, obj);
		} else {
			clone = obj;
		}

	}

	return clone;

}

How it works

First, let’s create a deepMerge() helper function.

Because we want to accept one or more arrays or objects as arguments, we’ll use a rest parameter, ...objs, to catch them all and turn them into an array.

function deepMerge (...objs) {
	// ...
}

Next, we’ll use the Array.shift() method to get the first item from the objs array and remove it from the array.

We’ll pass it into the structuredClone() method to create a deep copy, and assign it to the clone variable.

This is the object or array that we’ll merge all other items into, so we’ll return it back out.

function deepMerge (...objs) {

	// Create a clone of the first item in the objs array
	let clone = structuredClone(objs.shift());

	return clone;

}

Looping over the other arrays or objects

Next, we want to loop through each array or object in the objs array, and merge their values into the clone.

We’ll use a for...of loop to do that.

function deepMerge (...objs) {

	// Create a clone of the first item in the objs array
	let clone = structuredClone(objs.shift());

	// Loop through each item
	for (let obj of objs) {
		// ...
	}

	return clone;

}

Inside the loop, we want to determine if the obj is an array, an object, or a primitive.

The typeof operator returns object for many types of items, not just plain objects ({}). We’ll instead use the Object.prototype.toString.call() approach.

To make this easier, we’ll add a getType() helper function.

/**
 * Get the object type
 * @param  {*}       obj The object
 * @return {String}      The object type
 */
function getType (obj) {
	return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}

Then, inside the loop, we’ll check the type of the obj and clone.

If the clone and obj are not the same type of object, we’ll replace the clone with the current obj in the loop. Then we’ll continue to skip to the next item.

This would only happen if you passed in, for example, an object as the first argument, and an array as the second.

function deepMerge (...objs) {

	/**
	 * Get the object type
	 * @param  {*}       obj The object
	 * @return {String}      The object type
	 */
	function getType (obj) {
		return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
	}

	// Create a clone of the first item in the objs array
	let clone = structuredClone(objs.shift());

	// Loop through each item
	for (let obj of objs) {

		// Get the object type
		let type = getType(obj);

		// If the current item isn't the same type as the clone, replace it
		if (getType(clone) !== type) {
			clone = structuredClone(obj);
			continue;
		}

	}

	return clone;

}

Merging arrays

If the type of obj is an array ([]), we’ll pass the obj into the structuredClone() method to create a deep copy.

Then, we’ll use the spread operator to combine the clone and obj into a single array.

// Loop through each item
for (let obj of objs) {

	// Get the object type
	let type = getType(obj);

	// If the current item isn't the same type as the clone, replace it
	if (getType(clone) !== type) {
		clone = structuredClone(obj);
		continue;
	}

	// Otherwise, merge
	if (type === 'array') {
		clone = [...clone, ...structuredClone(obj)];
	}

}

Merging objects

If the type of obj is an object ({}), we need to loop through each item in it and do some additional checks.

To make that easier, let’s create a mergeObj() function. We’ll pass the clone and the current obj into it as argument.

// Loop through each item
for (let obj of objs) {

	// ...

	// Otherwise, merge
	if (type === 'array') {
		clone = [...clone, ...structuredClone(obj)];
	} else if (type === 'object') {
		mergeObj(clone, obj);
	}

}

Inside the mergeObj() function, we’ll use the Object.entries() method with array destructuring to loop over each item in the obj and get the key and value.

/**
 * Deep merge two objects
 * @return {Object}
 */
function mergeObj (clone, obj) {
	for (let [key, value] of Object.entries(obj)) {
		// ...
	}
}

For each item, we’ll create a copy of the value using the structuredClone() method, and assign that value to the matching key in the clone object.

/**
 * Deep merge two objects
 * @return {Object}
 */
function mergeObj (clone, obj) {
	for (let [key, value] of Object.entries(obj)) {
		clone[key] = structuredClone(value);
	}
}

This is the same as doing a shallow merge, though.

If the key already exists in the clone, and both the current item at that key and the new value are of the same type, and the value is an array or object, we want to recursively merge them together.

To do that, we’ll pass the clone[key] and the value back into the deepMerge() function, and assign the returned value to the clone[key] property.

/**
 * Deep merge two objects
 * @return {Object}
 */
function mergeObj (clone, obj) {
	for (let [key, value] of Object.entries(obj)) {
		let type = getType(value);
		if (clone[key] !== undefined && getType(clone[key]) === type && ['array', 'object'].includes(type)) {
			clone[key] = deepMerge(clone[key], value);
		} else {
			clone[key] = structuredClone(value);
		}
	}
}

Everything else

Finally, for all other object types, we’ll update the value of clone to the current obj.

// Loop through each item
for (let obj of objs) {

	// ...

	// Otherwise, merge
	if (type === 'array') {
		clone = [...clone, ...structuredClone(obj)];
	} else if (type === 'object') {
		mergeObj(clone, obj);
	} else {
		clone = obj;
	}

}

Preorder my new course on Web Components! Want to learn how to build Web Components from scratch, master best practices, and more? Preorder today and get $100 off of the launch price.


Find this useful? You can support my work by purchasing an annual membership.