Iteration and array transformation · Lesson 32 of 77 · practice
map: one array in, a new array out
About 60 minutes.
By the end of this lesson you can
- Use map to build a new array from an existing one without touching the original.
- State the relationship between the input length and the output length.
- Return a value from every callback, and recognise the array of undefined that appears when you forget.
- Choose map over forEach when the results are what you want.
- Avoid passing a built-in function straight to map when it accepts more than one argument.
Same shape, different contents
Why it matters
A very large share of real programming is one job: I have a list of things, and I need a list of slightly different things. Prices in pounds, and I need prices with tax. User records, and I need just their names. Raw measurements, and I need them rounded. The list is the same length, in the same order — only each item has been changed.
You already know how to do that with a loop: make an empty array, walk the original, push a transformed value onto the new one. You wrote that function in the last lesson. It is four lines of bookkeeping around one line of actual work, and it is so common that JavaScript gives it a name.
That name is map. It is the single most used array method in the language, and once it clicks you will see the pattern everywhere.
A machine on a conveyor belt
An analogy
Picture a conveyor belt carrying items past a machine. Each item goes in, a changed item comes out, and it lands on a second belt in the same position it had on the first. Nothing is added, nothing is dropped, nothing is reordered. When the first belt is empty, the second belt holds exactly as many items as the first one did.
You supply the machine — that is your callback. map supplies the belts. And crucially, the original belt still has its items on it: map does not consume or alter the array it was called on. What you get back is new.
map returns a new array
Explanation
You write const changed = original.map(callback). map calls your callback once per element, in order, and collects each returned value into a new array, which it hands back. The original is untouched.
Two consequences follow, and both are load-bearing. First, the output always has exactly the same length as the input. map cannot drop items — that is filter's job, two lessons from now — and it cannot add them. If you find yourself wanting a shorter array out, you have reached for the wrong method. Second, because the result is a genuinely new array, you must assign it to something or pass it somewhere. Calling map and ignoring what it returns does nothing useful at all.
The callback receives the same three arguments as forEach: the element, its index, and the array. And as before, take only what you need.
Pulling one field out of every record
Explanation
The commonest map of all is not arithmetic, it is extraction: an array of objects goes in, an array of one field goes out. Given a list of user objects, users.map((user) => user.name) gives you a list of names.
This reads as a sentence, which is the point. A counting loop that builds the same list forces the reader to hold a counter, a length and an index expression in their head just to work out that the answer is 'the names'. The map version says it directly.
One piece of syntax to have ready: when the thing you want to return is an object literal, wrap it in parentheses. An arrow function written as (user) => { name: user.name } is not returning an object — the braces are read as a function body, and it returns undefined. Writing (user) => ({ name: user.name }) makes the intent unambiguous.
The three ways map goes wrong
A common mistake
Forgetting to return. An arrow function with a concise body — (n) => n * 10 — returns its expression automatically. Add braces and it becomes a full body, and a full body returns nothing unless you say return. So (n) => { n * 10 } produces an array of undefined, one per element. If a map result comes back the right length but full of nothing, this is why.
Using map for its side effects. If your callback prints, or pushes onto another array, and you do not use what map hands back, you have written a forEach with extra steps and an array nobody wanted. Say what you mean: forEach for effects, map for results.
Passing a built-in straight in. ['10', '10', '10'].map(parseInt) does not give [10, 10, 10]. map passes three arguments to whatever function it is given, and parseInt reads its second argument as a number base — so it is asked to parse in base 0, then base 1, then base 2. The result is [10, NaN, 2]. The fix is to wrap it yourself: .map((text) => parseInt(text, 10)), or use Number, which takes only one argument.
How the run pad prints an array
Explanation
One thing to know before you run the examples, so you do not misread them. The run pad in this product prints a value by converting it to JSON. That is why an array prints as [20,40,60] with no spaces, and why an object prints with quoted keys.
JSON has no way to write undefined, so anything undefined sitting inside an array prints as null. Something similar happens to NaN. That is the pad's display, not something JavaScript did to your data — print the element on its own and you will see undefined or NaN as expected. Being told this once is cheaper than spending an evening hunting for a null your code never created.
Check there: undefined and NaN are not valid JSON: undefined found in an array is changed to null, and NaN serialises as null.
One in, one out
A mental model
Hold on to one sentence: map is one in, one out, in order, into a new array. Every question you will have about map is answered by it. Can it filter? No — one in, one out. Can it reorder? No — in order. Does it change the original? No — into a new array.
When what you want does not fit that sentence, a different method fits instead, and the next two lessons supply them. Keeping the methods honest about what they each do is what makes a chain of them readable later.
Recap
Recap
map builds a new array by calling your function on every element and collecting what it returns. The result has the same length and order as the input, and the input is unchanged. A concise arrow body returns automatically; a braced body needs an explicit return, or every element comes back undefined. Use forEach when you want effects and map when you want results, and never hand map a built-in function that takes more than one argument.
Worked examples
Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.
A new array, and the old one still intact
The smallest possible map, printed alongside the original to prove the point that matters most: nothing was modified in place.
1const prices = [10, 20, 30];2const doubled = prices.map((price) => price * 2);3 4console.log(doubled);5console.log(prices);- line 2
A concise arrow body: price * 2 is returned automatically, with no return keyword and no braces.
- line 5
Unchanged. map read from this array and never wrote to it.
What it prints
[20,40,60][10,20,30]
Turning records into initials
Extraction, the most common use of map in real code. An array of objects goes in and an array of short strings comes out, one per record, same order.
1const people = [2 { first: "Ada", last: "Lovelace" },3 { first: "Alan", last: "Turing" },4];5 6const initials = people.map((person) => person.first[0] + person.last[0]);7 8console.log(initials);9console.log(initials.length === people.length);- line 6
person.first[0] is the first character of a string — strings are indexable by position, just like arrays.
- line 9
Always true for a map. The output length is the input length, no matter what the callback does.
What it prints
["AL","AT"]true
The two failures worth seeing once
First a braced arrow body with no return, then a built-in handed straight to map. Both run without error, which is exactly what makes them worth meeting deliberately rather than at midnight.
1const numbers = [1, 2, 3];2 3const broken = numbers.map((n) => {4 n * 10;5});6 7console.log(broken.length);8console.log(broken[0]);9 10const parsed = ["10", "10", "10"].map(parseInt);11console.log(parsed.join(","));12 13const fixed = ["10", "10", "10"].map((text) => parseInt(text, 10));14console.log(fixed.join(","));- line 3
Braces make this a full function body. It computes n * 10 and returns nothing, so every element of broken is undefined.
- line 8
Printed on its own it shows as undefined. Printed inside the array it would show as null, because JSON cannot represent undefined.
- line 10
map passes (element, index, array), so parseInt is called with radix 0, then 1, then 2. Base 1 is not a thing, and "10" in base 2 is 2.
- line 13
Wrapping it yourself fixes it, because now only the arguments you chose are passed on.
What it prints
3undefined10,NaN,210,10,10
Check yourself
Not scored, not stored. Getting one wrong is the useful part.
You call map on an array of 5 elements. How long is the array you get back?
One of these callbacks has braces and one does not. Predict both printed lines before running it.
const withBraces = [1, 2].map((n) => { n + 1;});const withoutBraces = [1, 2].map((n) => n + 1);console.log(withBraces[0]);console.log(withoutBraces);
You want to print every name in an array, and you do not need any array afterwards. Which method fits?
Write it yourself
Exercise · intro · 6 tests
Add VAT to every price
Write a function called withVat that takes an array of prices and returns a NEW array where each price has 20% VAT added and is then rounded to the nearest whole number with Math.round. So 10 becomes 12 and 9 becomes 11 (9 plus 20% is 10.8, which rounds to 11). An empty array gives an empty array, and the array you were given must not change.
withVat(prices) returns a new array of the same length and order, where each element is Math.round(price * 1.2). withVat([]) is []. The input array still holds its original values afterwards, and the returned array is not the same array object that was passed in.
Define withVat at the top level of your code — the tests call it by name.
Exercise · core · 6 tests
Number the entries with map
Write a function called rankedNames that takes an array of names and returns a NEW array of strings, each one the name prefixed with its position starting at 1 and a colon. So ["Ada", "Bo"] becomes ["1: Ada", "2: Bo"]. Mind the space after the colon. An empty array gives an empty array.
rankedNames(names) returns a new array of the same length and order, where the element at position i is (i + 1) + ": " + names[i]. rankedNames([]) is []. The input array is unchanged.
Define rankedNames at the top level of your code — the tests call it by name.
Worth remembering
- Summarise map in one sentence.
One in, one out, in order, into a new array. Same length as the input, same order, original untouched. It cannot drop elements (that is filter) and cannot combine them (that is reduce).
- Why does map sometimes return an array full of undefined?
Because the callback has a braced body with no return. (n) => n * 2 returns implicitly; (n) => { n * 2; } does not. Add the return keyword, or drop the braces.
- Why is ["10", "10", "10"].map(parseInt) not [10, 10, 10]?
map passes (element, index, array), and parseInt reads its second argument as a number base — so it parses in base 0, 1 and 2, giving [10, NaN, 2]. Wrap it: .map((t) => parseInt(t, 10)).
Check this lesson against the source
We wrote the explanation; we did not invent the facts. This is the page that backs them.
Check there: map creates a new array populated with the results of calling the callback on every element, does not modify the array it is called on, and passes (element, index, array) to the callback.
MDN — Arrow function expressions
Check there: A concise body returns its expression implicitly; a block body requires an explicit return, and an object literal must be wrapped in parentheses.
Check there: parseInt takes a second argument, radix, which is why passing it directly to map misbehaves.
Prefer to read the source in a structured breakdown? Turn this doc into a breakdown