How Lodash _.merge Handles Nested Arrays
This article explores the deep reconciliation strategies employed by
Lodash’s _.merge method when resolving conflicts between
nested array structures. Unlike shallow merging or naive array
concatenation, _.merge treats arrays using an index-based
recursive alignment model. Below, we break down the specific rules
Lodash applies to conflicting indices, how it handles varying array
lengths and primitive-versus-object values, and how developers can
override these defaults using custom reconcilers.
Index-Based Key Alignment
The foundational strategy of _.merge is treating arrays
as objects with sequential numeric keys (0, 1,
2, ...). When encountering an array in both the target and
the source at the same path, Lodash does not perform a list
concatenation or a mathematical union. Instead, it pairs elements by
their matching index:
target[0]is reconciled againstsource[0]target[1]is reconciled againstsource[1]target[n]is reconciled againstsource[n]
This strategy ensures that the position of an element dictates its merge counterpart, mirroring the behavior of named keys in standard JavaScript objects.
Value Resolution: Primitives vs. Objects
At each index position, Lodash determines how to reconcile the conflicting elements based on their data types:
- Primitive Overwrite: If the element at a given
index in the source is a primitive (string, number, boolean,
null, etc.), it replaces the value at that same index in the target. - Object and Array Recursion: If the elements at the
matching index in both the target and source are plain objects or
arrays,
_.mergedescends into them recursively. For nested objects, keys are merged deeply; for nested arrays, the index-matching strategy repeats. - Type Mismatch: If the target element is a primitive and the source element is an object or array (or vice versa), the source element completely replaces the target element, converting the type at that index.
Array Sizing and Residual Elements
When the target and source arrays differ in length,
_.merge applies an expansion strategy without truncating
existing data:
- Source is longer than Target: Lodash copies the additional trailing elements directly from the source into the target. The resulting array expands to match the length of the source array.
- Target is longer than Source: Lodash reconciles up to the index length of the source array. Any residual elements at indices beyond the source array’s length remain intact in the target. Lodash does not shrink arrays during a merge.
Deep Reconciliation Example
Consider a scenario where arrays contain nested objects:
const target = {
users: [
{ id: 1, preferences: { theme: 'light', alerts: true } },
{ id: 2, name: 'Alice' }
]
};
const source = {
users: [
{ preferences: { theme: 'dark' } },
{ id: 2, name: 'Alice', role: 'admin' },
{ id: 3, name: 'Bob' }
]
};When passed to _.merge(target, source), Lodash performs
the following steps:
- Index 0: Reconciles the objects.
{ id: 1 }is preserved, whilepreferences.themeis updated to'dark'andpreferences.alertsremainstrue. - Index 1: Merges the objects, preserving
idandnamewhile appendingrole: 'admin'. - Index 2: Since the target lacks index 2,
{ id: 3, name: 'Bob' }is appended to the target array.
Customizing Array
Strategies via _.mergeWith
Because the default index-keyed strategy can lead to accidental
mutations when developers expect array concatenation or deduplication,
Lodash provides _.mergeWith. This variant allows the
injection of a custom reconciler function:
import _ from 'lodash';
function customizer(objValue, srcValue) {
if (_.isArray(objValue)) {
return objValue.concat(srcValue);
}
}
_.mergeWith(target, source, customizer);By intercepting the reconciliation pipeline before the recursive index mapping occurs, developers can implement alternative strategies, such as set unions, unique element filtering, or complete array replacements.