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:
typeof value === 'number': This evaluates tofalsefornew Number().isObjectLike(value): This evaluates totruebecausevalue !== null && typeof value === 'object'.baseGetTag(value) === '[object Number]': Lodash invokes an internal abstraction overObject.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)); // trueBecause 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:
- Truthiness Traps: Primitive
0is falsy (Boolean(0) === false), but an object wrapper around zero is truthy (Boolean(new Number(0)) === true). If a developer relies on_.isNumberto validate input and later usesif (val)for control flow, a boxed zero executes the truthy branch. - Equality Checks: Primitive comparisons use value
comparison (
0 === 0), whereas boxed numbers use reference comparison (new Number(0) !== new Number(0)). - NaN Encapsulation: Calling
new Number(NaN)also returnstruefor_.isNumber, consistent with primitiveNaN, sinceNaNis technically considered a number type in JavaScript.