Tricky use case of Array.prototype.map in JS
If you are familiar with functional programming, Array.prototype.map must be a function that you work with every day.
For me, map() is a powerful function, but there might be some tricky use case of it that you don't know about!
Let's walk through some basic knowledge
The map() method creates a new array from the calling array by applying a provided callback function once for each element of the calling array
Simple use cases
Giving this array
let devs = [
{ id: '1', name: 'Leo', yob: 1995 },
{ id: '2', name: 'Paul', yob: 1995 },
{ id: '3', name: 'Jesse', yob: 1996 },
{ id: '4', name: 'Ken', yob: 1997 },
]
What can we do using map() function:
- Getting new values from array
let ages = devs.map((dev) => {
let currentYear = new Date().getFullYear()
return currentYear - dev.yob
})
console.log(ages) // => [25, 25, 24, 23]
- Extracting values from array of objects
let names = devs.map((dev) => dev.name)
console.log(names) // => ["Leo", "Paul", "Jesse", "Ken"]
- Rendering list in React application
import React from 'react'
export default DeveloperProfiles = ({ devs }) => {
return (
<ul>
{devs.map((dev) => (
<li key={dev.id}>{dev.name}</li>
))}
</ul>
)
}
Tricky use case
It is common to pass the pre-defined callback function as map() argument like this:
let extractId = (dev) => {
return dev.id
}
let ids = devs.map(extractId)
console.log(ids) // => ["1", "2", "3", "4"]
But this habit may lead to unexpected behaviors with functions that take additional arguments.
Consider this case - we need to get all ids as numbers:
// ids = ["1", "2", "3", "4"]
let idsInNumber = ids.map(parseInt)
console.log(idsInNumber) // => ???
You might expect the result is [1, 2, 3, 4]
, but the actual result is [1, NaN, NaN, NaN]
We encountered this problem at Cốc Cốc recently, it took me a while to figure out, and here's the answer...
Usually, we use map() callback with 1 argument (the element being traversed), but Array.prototype.map passes 3 arguments:
- the element
- the index
- the calling array (which we don't use most of the time)
And the callback in this case - parseInt is often used with 1 argument but it takes 2:
// Syntax
parseInt(string [, radix])
string
: the value to parseradix
: [Optional]: an integer that represent the radix (the base in mathematical numeral systems) - usually, it's 10
For example:
parseInt('10') // => 10
parseInt('10', 10) // => 10
parseInt('10', 2) // => 2
parseInt('10', 4) // => 4
And here you can see the source of confusion !
The third argument of map() is ignored by parseInt - but not the second one!
Hence, the iteration steps of map() look like this:
// map(parseInt) => map(parseInt(value, index))
/* index is 0 */ parseInt('1', 0) // => 1
/* index is 1 */ parseInt('2', 1) // => NaN
/* index is 2 */ parseInt('3', 2) // => NaN
/* index is 3 */ parseInt('4', 3) // => NaN
Solution
As you've known the source of the problem, the solution is not to pass all of the map()'s arguments to your callback if you're not sure how it works
- Passing only the arguments that your callback needs
function returnInt(element) {
return parseInt(element, 10)
}
;['1', '2', '3', '4'].map(returnInt)
// => [1, 2, 3, 4]
// Same as above, but using the concise arrow function syntax
// Better use this if you don't need to re-use the callback
;['1', '2', '3', '4'].map((numb) => parseInt(numb, 10))
// => [1, 2, 3, 4]
- A simpler way to achieve our case like Airbnb style guide
;['1', '2', '3', '4'].map(Number)
// => [1, 2, 3, 4]
And that's what I know about Array.prototype.map function, feel free to share your use cases in the comment section
Happy coding!