Leo's dev blog

Tricky use case of Array.prototype.map in JS

Boy playing jenga
Published on
/4 mins read/---

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 parse
  • radix: [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]
;['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!

Refs