How Lodash _.set Chooses Between Arrays and Objects
Lodash's _.set utility dynamically builds intermediate
structures when traversing a path that does not yet exist. Rather than
inspecting the current key, the method definitively infers whether to
instantiate a nested Array or a nested plain Object by looking ahead to
the next key in the path sequence. If that subsequent key
qualifies as a valid array index, Lodash creates an empty Array;
otherwise, it defaults to creating a plain Object.
The Lookahead Mechanism
When _.set(object, path, value) executes, it parses the
path into an array of keys (for example, 'users[0].name'
becomes ['users', '0', 'name']) and iterates through them
via its internal baseSet function.
As it traverses, if the property corresponding to the current key is
undefined or null, Lodash must dynamically
instantiate a container. To determine the container type:
- It checks the next key immediately following the current segment in the path array.
- It evaluates that next key using an internal utility function named
isIndex. - If
isIndex(nextKey)returnstrue, the current missing property is assigned an empty Array ([]). - If
isIndex(nextKey)returnsfalse, the property is assigned an empty Object ({}).
How Lodash Defines an Index
The internal isIndex helper strictly validates whether a
given key represents a non-negative integer index. A key is considered
an index if:
- It is a number or a string that matches the regular expression
^(?:0|[1-9]\d*)$. - It is greater than or equal to
0. - It is less than or equal to
Number.MAX_SAFE_INTEGER(9007199254740991). - It is not a floating-point number, a negative number, or a non-numeric string.
Because isIndex accepts numeric strings, string
representations of non-negative integers (such as '0' or
'12') trigger array creation identically to integer
literals.
Practical Examples
Lookahead to an Index
const obj = {};
_.set(obj, 'data.items[0]', 'value');- Step 1: Evaluates
'data'. It is undefined. The next key is'items', which is not an index.obj.databecomes{}. - Step 2: Evaluates
'items'. It is undefined. The next key is'0', which satisfiesisIndex.obj.data.itemsbecomes[]. - Step 3: Sets index
0ofobj.data.itemsto'value'.
Result: { data: { items: ['value'] } }
Lookahead to a Non-Index
const obj = {};
_.set(obj, 'items[0].name', 'Alice');- Step 1: Evaluates
'items'. Next key is'0'.obj.itemsbecomes[]. - Step 2: Evaluates index
'0'. It is undefined. The next key is'name', which failsisIndex.obj.items[0]becomes{}. - Step 3: Sets
obj.items[0].nameto'Alice'.
Result: { items: [{ name: 'Alice' }] }
Edge Cases: Negative Numbers and Non-Integers
If a key contains negative numbers or floating-point representations,
isIndex returns false:
const obj = {};
_.set(obj, 'values[-1].id', 123);Because '-1' is not a non-negative integer, the
lookahead fails the index test, causing obj.values to be
initialized as an Object ({ '-1': { id: 123 } }) rather
than an Array.