How Lodash assignInWith Resolves Key Conflicts

Lodash’s _.assignInWith provides a mechanism for merging multiple source objects into a destination object while copying both own and inherited enumerable properties. When multiple sources contain identical keys, default assignment behavior simply overwrites earlier values with the latest one encountered. By supplying a customizer function, developers can intercept each collision and implement custom conflict resolution strategies—such as combining values, prioritizing specific data types, or enforcing validation rules—evaluated sequentially across all source objects.

Mechanics of _.assignInWith

The _.assignInWith method behaves similarly to _.assignIn (historically known as _.extend), but accepts a callback function as its final argument:

_.assignInWith(object, ...sources, [customizer])

Unlike _.assignWith, which only copies own enumerable string-keyed properties, _.assignInWith iterates over both own and inherited enumerable string-keyed properties from each source.

The Customizer Signature

When a property is assigned, the customizer function is invoked with the following arguments:

  1. objValue: The current value of the key on the destination object.
  2. srcValue: The value of the key from the current source object being evaluated.
  3. key: The key being assigned.
  4. object: The destination object.
  5. source: The source object currently being read.

If the customizer returns undefined, Lodash falls back to its default behavior, overwriting objValue with srcValue. If any other value is returned, that value is assigned to the key on the destination object.

How Conflicts Are Resolved Across Multiple Sources

When resolving identical keys across more than one source object, _.assignInWith processes arguments sequentially from left to right. This left-to-right evaluation creates an accumulator-style resolution cycle:

  1. First Collision: When the first source containing key K is read, objValue is the initial value of destination[K], and srcValue is source1[K]. The customizer computes the new value and writes it to destination[K].
  2. Subsequent Collisions: When a second source containing key K is read, objValue is no longer the original target value; it is the resolved value produced from the previous step. srcValue is now source2[K].
  3. Chained Resolution: This cycle repeats for every subsequent source containing key K. Each step receives the current accumulated state of the destination property alongside the incoming source property.

Step-by-Step Code Example

The following example demonstrates merging array values and handling scalar collisions sequentially across three objects:

const _ = require('lodash');

function customResolver(objValue, srcValue, key) {
  // If both values are arrays, concatenate them
  if (_.isArray(objValue) && _.isArray(srcValue)) {
    return objValue.concat(srcValue);
  }
  // If both are numbers, take the maximum value
  if (_.isNumber(objValue) && _.isNumber(srcValue)) {
    return Math.max(objValue, srcValue);
  }
  // Return undefined to let default assignment handle other cases
  return undefined;
}

const target = { tags: ['admin'], score: 10 };
const sourceA = { tags: ['moderator'], score: 25 };
const sourceB = { tags: ['user'], score: 15 };

const result = _.assignInWith(target, sourceA, sourceB, customResolver);

Execution Flow for score:

  1. Initial State: target.score is 10.
  2. Processing sourceA: The customizer is called with objValue = 10 and srcValue = 25. Math.max(10, 25) returns 25. target.score becomes 25.
  3. Processing sourceB: The customizer is called with objValue = 25 and srcValue = 15. Math.max(25, 15) returns 25. target.score remains 25.

Execution Flow for tags:

  1. Initial State: target.tags is ['admin'].
  2. Processing sourceA: customizer(['admin'], ['moderator']) returns ['admin', 'moderator'].
  3. Processing sourceB: customizer(['admin', 'moderator'], ['user']) returns ['admin', 'moderator', 'user'].

Handling Inherited Properties

Because _.assignInWith traverses the prototype chain of each source object, identical keys defined on prototype chains will trigger the customizer function just like own properties.

If a source object inherits a property that shadows a property on the target, the customizer still receives the inherited property as srcValue. To prevent prototype pollution or unintended merges from inherited keys, customizer functions can inspect whether a property is an own property using Object.prototype.hasOwnProperty.call(source, key) before applying resolution logic.