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:

  1. For shared indices, the destination array's values take precedence. If a destination element is not undefined, it remains unchanged.
  2. Once the traversal exceeds the bounds of the destination array, the remaining indices in the destination are undefined.
  3. 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:

  1. Lodash checks each index defined in the source array.
  2. 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).
  3. Traversal of the source finishes once its final index is checked.
  4. 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.