Published on 01/08/2021 by

Nevulo profile picture
Nevulo

Numbers in JavaScript; harder than you think

Simple, innocuous methods which will trip you up when working with numbers in JavaScript

Numbers in JavaScript; tougher than you think

JavaScript (JS) isn't exactly

for being robust or terse - it's quite the opposite, it stands behind the notion of "I'll take whatever you throw at me". JS is a , developers won't have to explicitly define the type of data they're using. This can make things a lot simpler, but also a lot more complicated at the same time as we don't know what the data we are receiving will be or what we'll be able to do with it.

Let's take a simple addNumbers function:

1function addNumbers(first, second) {
2 return first + second;
3}
1addNumbers(1, 2); // logs 3
2typeof addNumbers(1, 2); // logs "number"

Great! We can feed two arguments in as numbers and get the sum as a number for the result. That means we can do number specific operations like .toFixed. But if we pass in a string as the first argument...

1addNumbers("1", 2); // logs "12"
2typeof addNumbers("1", 2); // logs "string"

Now our add function, intended for adding numbers is returning text! Even worse, if we were expecting a number to be returned and we tried using a number-specific method like .toFixed, our app would throw an error:

addNumbers(...).toFixed is not a function

The situation above is unlikely to happen but not impossible (developer mistake). What is more likely is your app accepting user input, and we need to add validation in our addNumbers function so that our app isn't just trying to add any two things together. But how can we transform the two arguments in addNumbers into numbers and then confirm they are valid?

Parsing strings as numbers

parseInt

parseInt (also available under Number.parseInt with same functionality) accepts two parameters, the first being the value to attempt to coerce into a number and the second is the radix or base.

1parseInt("100", 10); // 100

It's good to explicitly set the radix as 10 (since we count in base 10) as if the input string begins with "0x", this represents an octal and the default radix will change to 16 (hexadecimal), producing different results.

An oddity to note with parseInt is that it will parse the string character by character until it finds an invalid character for the specified radix. To show this better, see below how the same input can result in two different outputs:

1parseInt("0xf", 10); // 0, stops when reaching "x"
2parseInt("99blake", 10); // 99, stops when reaching "b"
3parseInt("0xf", 16); // 15, 0xf is 15 in hexadecimal (base 16)

Validating if a value is a number

isNaN()

isNaN is a function available in the global scope and it allows us to know if a given input is "not a number" (NaN). We can inverse this to check if an input is a number.

1!isNaN(1); // === true, 1 is a number!

Great, we should expect to see that entering a string would return false because a string is obviously not a number.

1!isNaN(""); // === true, what??

This function only validates that the value, when coerced to a number using the Number constructor does not strictly equal itself. A quick reminder on a weird quirk: NaN is the only value in JS that does not strictly equal itself:

1NaN === NaN; // false

Internally, isNaN works similarly to this pseudocode:

1function parseInt(value) {
2 const n = Number(value);
3 return n !== n;
4}

So when we convert an empty string to a number using the Number constructor, what do we get?

1Number(""); // 0
20 === 0; // true
3// therefore..
4isNaN(""); // false

If you actually only want to test that a value is/isn't strictly NaN, prefer Number.isNaN() instead, which is more robust and also ensures that the type of the value is number using typeof

typeof x === "number"

The typeof operator returns the type of the data after it as a string:

1let a = 1;
2typeof a; // "number"

This is more accurate than isNaN as it will not accept strings since typeof (any string) evaluates to "string"

Number.isFinite and Number.isInteger

Number.isFinite only returns true if the value given is of type "number" and the value is within positive infinity and negative infinity (basically any valid number).

If you want to ensure that your data is strictly an integer (not a floating point number like 2.31412), prefer Number.isInteger instead which will return false when provided with a floating point number that cannot be represented as an integer. Note that floating point numbers that are large enough will be rounded and then can be represented as an integer, thus passing the check:

1Number.isInteger(5.0); // true
2Number.isInteger(5.000000000000001); // false
3Number.isInteger(5.0000000000000001); // true

Updating our function

1function addNumbers(first, second) {
2 // parseFloat allows us to convert a string into
3 // a number with floating point precision
4 first = parseFloat(first);
5 second = parseFloat(second);
6 // are the arguments valid numbers after being converted from a string?
7 if (!Number.isFinite(first))
8 throw new Error("first argument must be an integer");
9 if (!Number.isFinite(second))
10 throw new Error("second argument must be an integer");
11 return first + second;
12}
1addNumbers("1", 2); // 3! yay
2// also works with other wonky combinations
3addNumbers("1", "1"); // 2
4addNumbers("1", "1"); // 2
5addNumbers(1, "1"); // 2
6// errors when passing an invalid number
7addNumbers("blablah blah", 2);
8addNumbers("-", 2); // Error: first argument must be an integer
9addNumbers(1); // Error: second argument must be an integer

Comments

0

You need to be signed in to comment