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:
'apple'producesundefined. Sinceundefinedhas not been seen,'apple'is kept.'banana','cherry', and'date'also produceundefined.- 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:
'cat'[0]evaluates to'c'.'cat'is added.'car'[0]evaluates to'c'. This is a duplicate, so'car'is dropped.'dog'[0]evaluates to'd'. This is unique, so'dog'is added.'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:
- Validate data types: Ensure the collection elements match the expected shape of the iteratee prior to union operations.
- Provide fallback values: Use an iteratee function
with safe checks, such as
item => (typeof item === 'object' && item !== null ? item.value : item). - Use
_.union: If the array contains primitive strings and does not require property extraction, use standard_.unioninstead of_.unionBy.