Why can't I use parseInt with map in Javascript?
I recently had a case where I wanted to break an IP address into the numerical values of its octets. I started with a string like this:
'192.168.1.1';
with the goal of having an array with each of the values like this:
[192, 168, 1, 1];
My first attempt was to split the string on the dots making an array. Then, I had an array of the octets as strings. To get an array of numbers, I decided to run the array’s map
method passing in the parseInt
function. Here’s what that might look like:
const ip = '192.168.1.1';
const ipArray = ip.split('.').map(parseInt);
The results were not what I expected.
[192, NaN, 1, 1];
What makes this more puzzling is that each octet worked except for the second which yielded NaN
(not a number). Why does this happen? To understand this problem, we need a better understanding of both map
and parseInt
.
Intro to map()
Javascript arrays have a handy map
method that generates a new array by running each value in the old array through a function (the callback passed into map
). Here’s a quick example that doubles each value in an array of numbers:
const numbers = [1, 2, 3, 4];
const numbersDoubled = numbers.map((currentNumber) => currentNumber * 2);
console.log(numbersDoubled); // [2, 4, 6, 8]
The callback function takes a parameter currentNumber
and returns the value multiplied by 2. The map
method of the array passes each value in turn to our doubling callback function and returns a new array of the results returned from the callback.
Intro to parseInt()
parseInt
parses an integer from a string. This is good when you’re dealing with user input as you’ll usually get it as a string. You might want to use it as a number though. Here’s a quick example that doubles a user-entered number:
const userNumberAsString = prompt('Enter a number:');
const userNumber = parseInt(userNumberAsString);
const numberDoubled = userNumber * 2;
console.log(numbersDoubled); // Logs the number the user entered multiplied by 2
Fleshing Out Our Understanding
What we expected to happen when we ran const ipArray = ip.split('.').map(parseInt);
is for Javascript to iterate over the array passing each of the octet strings to parseInt
resulting in the string values being converted to numeric values. Let’s review the documentation for both map
and parseInt
to validate our assumptions about how these work.
map()
The map
documentation gives us some information about how the callback is called that we might not know even if we frequently use it. (I frequently forget it myself even though I use map
routinely.)
callback
is invoked with three arguments: the value of the element, the index of the element, and the Array object being traversed.
I generally use map
by passing in an anonymous function, and I typically don’t use the index or the array object. That makes it easy for me to forget they are being passed. Here, though, it could be the reason we’re getting odd results from parseInt
. I believe parseInt
takes a single argument: the string to be parsed into an integer, but I need to confirm that.
parseInt()
Once again, my understanding here is incomplete. From the parseInt
documentation, we learn it actually takes two arguments. The second argument is the radix of the first argument:
An integer between 2 and 36 that represents the radix (the base in mathematical numeral systems) of the above mentioned string.
What’s Actually Happening
With our new understanding, we can dissect the problem. parseInt
takes the radix as the second argument. map
is passing three arguments, the second of which is the index of the current item. The index of the current octet in the array is being passed to parseInt
although it expects the radix of the string being parsed instead.
Want more information about the radix? It’s the same as the base of another number system. Still lost? I’ve got a post on other number systems that will help.
Here are the parseInt
calls map
is making for our example IP address array:
parseInt('192', 0); // 192
parseInt('168', 1); // NaN
parseInt('1', 2); / 1
parseInt('1', 3); / 1
The next question to help us understand is this: why does it work for all the octets except the second one?
The first octet works because the 0
radix argument is falsey. (Javascript evaluates 0
as false
when it’s evaluated as a boolean.) The effect is the same as not passing a radix argument.
The second octet is the one that fails. We know from the documentation that Javascript expects a value between 2
and 36
for the radix. parseInt
isn’t implemented to parse an integer with a radix value of 1
, so it returns NaN
.
The third and fourth octets both work just because their values happen to be '1'
. '1'
is represented the same regardless of the radix. If these had been different numbers, you would have seen more NaN
values in the results.
How to Fix It
Now that we understand the problem, we can write a solution. Rather than passing the parseInt
function to the array’s map
method, we’ll pass an anonymous function that calls parseInt
with exactly the arguments we want. Here’s our fixed array mapping:
const ip = '192.168.1.1';
const ipArray = ip.split('.').map(octet => parseInt(octet, 10));
We’re only using the first value passed to the map
’s callback (because we don’t care about the index or the entire array). From the anonymous function, we’re returning the result of parseInt(octet, 10)
, with octet
being the string of that octet of the IP. I specified the radix here because of something else I picked up from the parseInt
documentation on MDN. If you read the entire description of the parseInt
function’s radix
parameter, you’ll find this:
Always specify this parameter to eliminate reader confusion and to guarantee predictable behavior. Different implementations produce different results when a radix is not specified, usually defaulting the value to
10
.
If we don’t specify the radix of the integer we’re parsing, the results could be unpredictable. In practice, I haven’t found that to be the case, but better to be explicit than rely upon different implementations having the same default value for radix.
Lessons Learned
We learned map passes three arguments to the callback rather than just the current value. We learned parseInt takes a second argument. More importantly than both of those, though, we learned that, if the results of your code are not what you expect, make sure you reference the documentation for anything you didn’t write yourself (in our case, map
and parseInt
) to be sure that your assumptions about how it works are correct.
Making these kinds of assumptions generally makes writing code much faster, but it can lead to problems like the ones we experienced here. As long as you know how to pull back and rebuild your mental models, you’ll be able to fix your issues and move forward quickly.