How Lodash _.merge Recursively Combines Objects

Lodash's _.merge method is a utility designed to deeply combine properties from one or more source objects into a destination object. Unlike shallow merge utilities, _.merge inspects the data types of matching keys, recursively traversing nested objects and arrays rather than replacing entire child structures. This article breaks down the exact mechanics of how _.merge walks nested object trees, resolves conflicting data types, handles arrays, and mutates target data.

The Core Recursive Traversal Engine

When _.merge(target, ...sources) is invoked, Lodash iterates over the enumerable own and inherited string/symbol keyed properties of each source object.

For every key-value pair encountered, the method inspects the destination object's corresponding property:

  1. Both Values are Plain Objects: If both the target value and the source value are mergeable objects (such as plain JavaScript objects), _.merge does not overwrite the property. Instead, it recurses into both values, repeating the merge operation at the deeper hierarchy level.
  2. Value Type Mismatches or Primitives: If either the source or target value is a primitive (such as a string, number, or boolean), or an object that is not safely mergeable (like DOM elements or functions), the source value completely overwrites the target value.
  3. Missing Keys: If a key exists in the source but does not exist in the target, the property and its nested structure are copied directly over to the target.

Array Handling and Index-Based Merging

A common point of confusion is how _.merge handles arrays. Instead of concatenating arrays or replacing the target array with the source array, _.merge treats arrays like index-keyed objects.

const target = { items: [{ id: 1, name: "Alpha" }, "two"] };
const source = { items: [{ name: "Omega", status: "active" }, "replaced", "three"] };

_.merge(target, source);
// Result:
// {
//   items: [
//     { id: 1, name: "Omega", status: "active" },
//     "replaced",
//     "three"
//   ]
// }

Treatment of undefined and Non-Plain Objects

_.merge explicitly skips source properties that evaluate to undefined. If the target object has an existing value and the corresponding source property is undefined, the target's original value remains unchanged. However, explicit null values in the source will overwrite values in the target.

Complex native objects such as Date and RegExp instances are copied by value rather than deeply merged. Custom class instances with non-standard prototypes are generally overwritten rather than recursively merged to prevent prototype pollution and unintended side effects.

Target Mutation

_.merge operates by mutating the target object passed as the first argument, while also returning that mutated object as the final result. If an immutable operation is required, an empty object literal should be supplied as the initial target:

const finalObject = _.merge({}, originalObject, updates);

By leveraging this recursive algorithm, _.merge allows complex, deeply nested state configurations and data structures to be patched cleanly without manually maintaining or rebuilding intermediate object hierarchies.