Lodash Type Coercion in Math Operations Like _.add

This article provides an overview of how the Lodash library manages explicit and implicit type coercion when complex objects are passed to mathematical utilities such as _.add. It details the internal pipeline Lodash employs to process arguments, explains how it delegates object conversion to native ECMAScript coercion protocols, and highlights how custom object methods like valueOf, toString, and Symbol.toPrimitive determine the final result of these operations.

The Architecture of Lodash Math Functions

Lodash creates most of its binary mathematical functions—such as _.add, _.subtract, _.multiply, and _.divide—using an internal higher-order factory function named createMathOperation. This factory standardizes argument validation, default handling, and type normalization before executing the actual arithmetic operator.

When a function like _.add(augend, addend) is called, createMathOperation evaluates both inputs using the following rules:

  1. Undefined Handling: If both arguments are undefined, it returns a designated default (such as 0 for _.add). If only one operand is undefined, the defined operand is returned as-is without further coercion.
  2. String Precedence: If either argument is strictly of type 'string', both arguments are converted to strings via Lodash's internal baseToString helper, and native concatenation occurs.
  3. Numeric Coercion: If neither argument is a string, both arguments are passed to baseToNumber before the arithmetic operator executes.

Internal Coercion via baseToNumber

When complex objects (such as plain objects, arrays, or class instances) are provided, they pass into Lodash's internal baseToNumber utility. The implementation relies directly on the native JavaScript unary plus operator (+value):

function baseToNumber(value) {
  if (typeof value == 'number') {
    return value;
  }
  if (isSymbol(value)) {
    return NAN;
  }
  return +value;
}

Because Lodash applies +value to objects, it delegates the conversion process entirely to the ECMAScript ToNumber abstract operation, which in turn invokes ToPrimitive with a hint of "number".

The ToPrimitive Resolution Order for Objects

When an object is coerced to a number during baseToNumber, the JavaScript runtime evaluates the object's methods in a strict order:

  1. Symbol.toPrimitive: If defined on the object, the engine calls [Symbol.toPrimitive]('number'). The returned primitive is then converted directly to a number.
  2. valueOf(): If Symbol.toPrimitive is absent, the engine executes object.valueOf(). If valueOf() returns a primitive value (such as a number, boolean, or string), that value is converted to a number.
  3. toString(): If valueOf() does not return a primitive (by default, Object.prototype.valueOf returns the object itself), the engine falls back to calling object.toString(). The resulting string is then parsed as a numeric literal.
  4. TypeError: If neither valueOf() nor toString() produces a primitive, the runtime throws a TypeError.

Coercion Behavior Across Different Object Types

Plain Objects

By default, plain objects inherit Object.prototype.toString, which returns "[object Object]". Because "[object Object]" cannot be parsed as a valid numeric literal, native coercion produces NaN:

const objA = {};
const objB = { value: 10 };

_.add(objA, 5); // Returns NaN
_.add(objB, 5); // Returns NaN

Objects with Custom Value Handlers

When an object defines a custom valueOf or [Symbol.toPrimitive] method, Lodash respects the returned value:

const item = {
  amount: 42,
  valueOf() {
    return this.amount;
  }
};

_.add(item, 8); // Returns 50

If [Symbol.toPrimitive] is implemented, it takes precedence over valueOf:

const advancedItem = {
  amount: 42,
  valueOf() {
    return 100;
  },
  [Symbol.toPrimitive](hint) {
    return hint === 'number' ? this.amount : 0;
  }
};

_.add(advancedItem, 8); // Returns 50

Arrays

Arrays inherit Array.prototype.toString, which joins elements with commas:

Edge Cases: Objects Coercing to Strings First

If one of the operands is an explicit primitive string, Lodash bypasses baseToNumber and forces string conversion on the other operand via baseToString:

const item = {
  valueOf() {
    return 10;
  },
  toString() {
    return 'custom';
  }
};

// String branch triggered by '5'
_.add(item, '5'); // Returns "custom5"

In this scenario, baseToString calls object.toString() directly rather than evaluating valueOf(), resulting in string concatenation instead of numeric addition.