Permalink
Branch: master
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
158 lines (117 sloc) 7.8 KB

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.