How Lodash defaultsDeep Handles Mismatched Arrays
This article explores how Lodash’s _.defaultsDeep method
behaves when merging structures containing arrays with significantly
different lengths. Rather than replacing or concatenating arrays as many
developers might expect, _.defaultsDeep treats arrays as
object collections with integer keys, resulting in an index-by-index
merge. The following sections break down the exact mechanics, the
outcome when destination arrays are shorter or longer than source
arrays, and the side effects of this behavior in complex data
structures.
Index-Based Traversal
Lodash does not treat JavaScript arrays as atomic values during a
deep default operation. Instead, _.defaultsDeep traverses
arrays identically to plain objects, using array indices
(0, 1, 2, and so on) as property
keys. When two arrays are encountered at the same property path, Lodash
evaluates them position by position rather than evaluating the array as
a unified set of data.
Because _.defaultsDeep only populates values that
evaluate to undefined in the destination, any index already
occupied in the target array is preserved.
Destination Shorter Than Source
When the destination array has fewer elements than the source array, the behavior proceeds as follows:
- For shared indices, the destination array's values take precedence.
If a destination element is not
undefined, it remains unchanged. - Once the traversal exceeds the bounds of the destination array, the remaining indices in the destination are undefined.
- Lodash copies the remaining elements from the source array into the destination array at their respective indices.
As a result, the destination array's length expands to match the length of the source array. The final output is a hybrid: the beginning of the array contains the original destination elements, and the remainder contains the trailing elements of the source array.
const object = { tags: ['admin', 'editor'] };
const defaults = { tags: ['guest', 'user', 'moderator', 'contributor'] };
_.defaultsDeep(object, defaults);
// Output: { tags: ['admin', 'editor', 'moderator', 'contributor'] }Destination Longer Than Source
When the destination array contains substantially more elements than the source array, the source array has minimal impact:
- Lodash checks each index defined in the source array.
- Because the destination array already possesses defined values at
all of those initial indices, none of the source values are applied
(assuming no destination values are explicitly
undefined). - Traversal of the source finishes once its final index is checked.
- The remaining elements in the destination array are left intact.
The length of the destination array remains unchanged, and no values
from the source array appear in the final output unless an overlapping
destination index was set to undefined.
const object = { tags: ['admin', 'editor', 'moderator', 'contributor'] };
const defaults = { tags: ['guest'] };
_.defaultsDeep(object, defaults);
// Output: { tags: ['admin', 'editor', 'moderator', 'contributor'] }Nested Objects Inside Mismatched Arrays
If array elements are objects, the recursive nature of
_.defaultsDeep applies to those matching indices.
If the source and destination both have an object at index
0, their properties will be deeply defaulted against each
other. If the source array contains objects at indices that do not exist
in the destination array, those complete objects are cloned and appended
to the destination array.
const object = {
users: [{ id: 1, name: 'Alice' }]
};
const defaults = {
users: [
{ id: 99, role: 'viewer' },
{ id: 2, name: 'Bob', role: 'guest' }
]
};
_.defaultsDeep(object, defaults);
// Output:
// {
// users: [
// { id: 1, name: 'Alice', role: 'viewer' },
// { id: 2, name: 'Bob', role: 'guest' }
// ]
// }Considerations and Workarounds
Because index-aligned array merging rarely aligns with intended
business logic, relying on _.defaultsDeep for configuration
objects or datasets containing mismatched list lengths can lead to
subtle bugs. Corrupted sequential lists, unexpected duplicate entries,
or spliced data often result from this behavior.
To enforce replacement or concatenation instead, use
_.mergeWith or _.assignWith paired with a
custom customizer function that inspects whether the target and source
values are arrays.