Lodash unionBy with Strings and Numeric Iteratee

Passing an array of strings to Lodash’s _.unionBy when the iteratee expects numeric properties leads to unexpected deduplication results due to how JavaScript handles property access and type coercion on primitive strings. Depending on whether the iteratee accesses object keys, string character indices, or performs arithmetic, the strings will either resolve to undefined, a character at an index, or NaN. Because Lodash uses the SameValueZero equality comparison on the iteratee's output, elements yielding identical values are treated as duplicates, often collapsing the entire array down to a single element.

How Lodash _.unionBy Operates

The _.unionBy function computes unique elements across one or more arrays after passing each element through a specified iteratee function or property shorthand. It iterates sequentially through the inputs, evaluates the iteratee for each item, and checks if that computed value has already been registered. If the returned value has been seen, subsequent items producing that same criterion are discarded.

Scenario 1: Property Lookups for Non-Existent Keys

If the iteratee expects an object with a numeric property (such as using a property path 'id' or a function item => item.id), attempting to read that property on a string evaluates to undefined. In JavaScript, primitive strings auto-box into String wrapper objects, which do not contain custom numeric properties.

const _ = require('lodash');

const list1 = ['apple', 'banana'];
const list2 = ['cherry', 'date'];

// The iteratee looks for an 'id' property
const result = _.unionBy(list1, list2, 'id');
console.log(result);
// Output: ['apple']

In this case:

  1. 'apple' produces undefined. Since undefined has not been seen, 'apple' is kept.
  2. 'banana', 'cherry', and 'date' also produce undefined.
  3. Because undefined === undefined, all subsequent elements are discarded. The result contains only the very first string.

Scenario 2: Index-Based Numeric Lookups

If the iteratee uses a numeric index (such as 0 or '0'), JavaScript treats this as an index accessor on the string.

const list1 = ['cat', 'car'];
const list2 = ['dog', 'cow'];

// Accessing index 0 (the first character)
const result = _.unionBy(list1, list2, 0);
console.log(result);
// Output: ['cat', 'dog']

Here:

  1. 'cat'[0] evaluates to 'c'. 'cat' is added.
  2. 'car'[0] evaluates to 'c'. This is a duplicate, so 'car' is dropped.
  3. 'dog'[0] evaluates to 'd'. This is unique, so 'dog' is added.
  4. 'cow'[0] evaluates to 'c'. This is a duplicate, so 'cow' is dropped.

Instead of failing with an error, the function deduplicates based on the character located at that numeric index.

Scenario 3: Mathematical Operations and Coercion

If the iteratee executes a mathematical computation expecting numbers (such as val => val * 2 or Math.round), passing non-numeric strings results in NaN.

const list1 = ['first', 'second'];
const list2 = ['third'];

const result = _.unionBy(list1, list2, x => Number(x));
console.log(result);
// Output: ['first']

Because non-numeric strings coerce to NaN, and Lodash treats NaN as equal to NaN using SameValueZero, only the first element yielding NaN is retained. If the strings contain valid numeric representations (e.g., ['10', '20']), they coerce to valid numbers and deduplicate according to their parsed values.

Preventing Unexpected Deduplication

To avoid unintended data loss when handling arrays where types may vary: