Converting a string to a number in JavaScript is surprisingly subtle. With NaN
, implicit radixes, and numbers vs Numbers, there are a lot of ways to shoot yourself in the foot. In this article, I'll cover the tradeoffs of parseFloat()
vs Number()
and Number.isNaN()
vs isNaN()
. I'll also describe how to enforce these rules with eslint.
The TLDR is you should use [Number(x)
] to convert generic JavaScript values to numbers if you want to be permissive, or parseFloat(x)
if you want to be more strict. You should always use Number.isNaN()
to check if the conversion failed. You should not use the global isNaN()
function.
typeof parseFloat('42'); // 'number'
Number.isNaN(Number('42')); // false
typeof parseFloat('fail'); // 'number'
Number.isNaN(Number('fail')); // true
Using Number(x)
has several edge cases that may be correct depending on your perspective. You can also use a tool like archetype that handles some of these edge cases for you:
archetype.to('42', 'number'); // 42
Number(''); // 0
archetype.to('', 'number'); // throws
Many developers use +x
to convert a string to a number. The JavaScript language spec states that +x
is equivalent to Number(x)
.
+'42 fail'; // NaN
+({ valueOf: () => '42' }); // 42
+({ toString: () => '42' }); // 42
+(null); // 0
+(' '); // 0
What's Wrong With Number(x)
?
Number(x)
and parseFloat(x)
handle edge cases very differently. parseFloat()
is more permissive when it comes to accepting different strings:
Number('42 fail'); // NaN
parseFloat('42 fail'); // 42
parseInt('42 fail'); // 42
Number(' 10'); // 10
parseFloat(' 10'); // 10
parseInt(' 10'); // 10
You might mistakenly assume this means Number(x)
is safer and more strict. Unfortunately, Number(x)
is more lax when it comes to whitespace, null
, and other edge cases. It converts a lot of surprising values to 0. For example:
Number(null); // 0
Number(''); // 0
Number(' '); // 0
Number(false); // 0
Number({ toString: () => '' }); // 0
Number({ valueOf: () => ' ' }); // 0
This is because the JavaScript language spec has a fairly complex set of rules for converting values to numbers.
The rules for how parseFloat()
converts values are simpler. The interpretter must convert the value to a string, trim whitespace, and then check for the longest prefix that matches JavaScript's regular expression definition of a numeric literal.
// `parseInt()` behaves like `parseFloat()` on these values
parseFloat(null); // NaN
parseFloat(''); // NaN
parseFloat(' '); // NaN
parseFloat(false); // NaN
parseFloat({ toString: () => '' }); // NaN
parseFloat({ valueOf: () => ' ' }); // NaN
Number.isNaN()
vs isNaN()
Another nuance of converting values to numbers is that JavaScript doesn't throw an error if it fails to convert a value x
to a number. It instead returns a special value NaN
. To make things more confusing, the typeof
operator reports that NaN
is a 'number'
.
Number('fail'); // NaN
typeof Number('fail'); // number
The reason why Number.isNaN()
and isNaN()
exist is because ==
and ===
do not work as expected with NaN
.
Number('fail') == Number('fail'); // false
Number('fail') === Number('fail'); // false
Number('fail') == NaN; // false
NaN === NaN; // false
Number.isNaN()
was a new feature in ES6, but unfortunately didn't get much attention. Number.isNaN()
is more robust and you should use it instead of isNaN()
unless you explicitly mean to use isNaN()
.
// Need to use a function because checking `=== NaN` does **not** work
isNaN(Number('fail')); // true
Number.isNaN(Number('fail')); // true
Here's a handy analogy for understanding the difference: Number.isNaN()
is to isNaN()
as ===
is to ==
. The isNaN()
function converts the given value to a number before checking it the given number is equal to NaN
.
isNaN('fail'); // true
isNaN({}); // true
Number.isNaN('fail'); // false
Number.isNaN({}); // false
On the other hand, Number.isNaN(x)
returns false
if x
is not of type number. You can polyfill Number.isNaN()
using the below function:
Number.isNaN = function(x) {
return typeof x === 'number' && isNaN(x);
};
Conversely, isNaN(x)
is equivalent to Number.isNaN(Number(x))
.
When you're checking whether the result of Number(x)
or parseFloat(x)
is equal to NaN
, you're safe using isNaN()
because you already tried to convert the value to a number. But in general, you should prefer Number.isNaN()
over isNaN()
in the same way that you (hopefully) use ===
unless you really know you mean ==
.
ESLint Rules
You can configure eslint to force you to use Number.isNaN()
and your choice of Number()
or parseFloat()
using the no-restricted-globals
rule. There's more info on this GitHub issue. Below is an example of a .eslintrc.yml
that would disallow using global isNaN()
and parseFloat()
rules:
no-restricted-globals:
- error
- name: isNaN
message: Use `Number.isNaN()` instead
- name: parseFloat
message: Use `Number()` instead
Requiring parseFloat()
instead of Number()
is trickier, but doable with eslint's generic no-restricted-syntax
rule.
rules:
no-restricted-globals:
- error
- name: isNaN
message: Use `Number.isNaN()` instead
no-restricted-syntax:
- error
- selector: CallExpression[callee.name='Number']
message: Do not use `Number()`, use `parseFloat()` instead
Moving On
Converting a value to a number in JavaScript is filled with odd edge cases. If you don't want to think about edge cases, you're best off just using parseFloat()
and Number.isNaN()
. If you want to be more flexible, you can use Number()
. Personally, I just use archetype for this because I don't want to worry about checking for NaN
.
Can't keep up with what's going on in your node_modules
? Check out JSReport's Slack integration. JSReport posts to a Slack channel whenever an npm package you're watching, like lodash or webpack, publishes a new release.