Lodash isNumber and new Number Edge Case Explained

In JavaScript, creating numbers using the new Number() constructor produces an object wrapper rather than a primitive number, causing standard typeof checks to fail. This article explains the internal mechanism that allows new Number() to pass Lodash’s _.isNumber validation, details how Lodash handles object-wrapped primitives via internal tag inspection, and highlights the behavioral edge cases that arise from this implementation.

The Primitive vs. Object Distinction

In standard JavaScript, numbers are typically represented as primitives:

const primitiveNum = 42;
console.log(typeof primitiveNum); // 'number'

When instantiated with the new keyword, the Number constructor creates an object wrapper:

const objectNum = new Number(42);
console.log(typeof objectNum); // 'object'

Using standard language constructs like typeof value === 'number', an instance created via new Number() evaluates to 'object' and fails the check.

How Lodash Implements _.isNumber

Lodash explicitly accounts for boxed primitive objects. The source implementation of _.isNumber relies on a dual-condition check:

function isNumber(value) {
  return typeof value === 'number' ||
    (isObjectLike(value) && baseGetTag(value) === '[object Number]');
}

The edge case that permits new Number() to pass this check is Lodash's secondary fallback:

  1. typeof value === 'number': This evaluates to false for new Number().
  2. isObjectLike(value): This evaluates to true because value !== null && typeof value === 'object'.
  3. baseGetTag(value) === '[object Number]': Lodash invokes an internal abstraction over Object.prototype.toString.call(value).

Even though new Number() is an object in the prototype chain, its internal [[Class]] tag remains [object Number]. Because baseGetTag resolves this tag directly, the method returns true.

Default Constructor Evaluation

When new Number() is called without arguments, it instantiates an object wrapping the primitive value 0:

const defaultNum = new Number();
console.log(defaultNum.valueOf()); // 0
console.log(_.isNumber(defaultNum)); // true

Because it retains the [object Number] tag, it satisfies _.isNumber despite containing no explicitly supplied initial value.

Critical Gotchas and Implications

Allowing boxed numbers to pass validation introduces subtle behavior in JavaScript applications: