Q1After you run prices.map((price) => price * 2), what happens to the original prices?
map and filter — Transforming vs. Narrowing Down an Array
Learn map, which transforms every element, and filter, which keeps only the ones that match. Covers why the original stays intact, chaining, and the index argument.
Whether you're pulling tax-included prices out of a menu or picking out only the products in stock, you keep writing the same code: declare an empty array, then call push on it inside forEach.
This article covers map, which transforms elements, and filter, which keeps only the elements that match a condition.
Transforming Each Element Into a New Array — map
Say you want a café menu's pre-tax prices converted to tax-included prices. The result should be an array in the same order as the original three items, with only the prices changed. You can compute each price with forEach, but to collect the results you have to create an array yourself and keep track of it until the loop ends.
The array method map (a method that passes each element through a callback and returns a new array of the return values, in order) takes care of both creating the result array and looping. The function you pass only has to describe how to transform a single element and return the result.
const menuItems = [
{ name: "House Blend", price: 480 },
{ name: "Caffe Latte", price: 540 },
{ name: "Cheesecake", price: 620 },
];
// Put just the transformation for one item in a function
const toTaxIncluded = (item) => Math.floor(item.price * 1.1);
// toTaxIncluded is called once per element, and the return values fill the new array
const taxIncluded = menuItems.map(toTaxIncluded);
console.log(taxIncluded.join(", ")); // 528, 594, 682
console.log(taxIncluded.length); // 3
console.log(menuItems.length); // 3
taxIncluded in the same order as the original elements. map never changes the number of elements — only their values.map simply hands each element to the callback one at a time and adds whatever comes back to the new array, so the number of elements can't change. Narrowing an array down, which does change that number, takes a different method.
forEach Doesn't Give You a Return Value
forEach only calls the function you pass; it doesn't collect the return values. It always returns undefined, so even if you write const list = menuItems.forEach(toTaxIncluded);, list is undefined. To get the results back as an array, use map.
Keeping Only Matching Elements — filter and the Resulting Length
Say you want to list only the books that are in stock. map never changes the number of elements, so it can't drop the out-of-stock ones, and you're back to writing an if inside forEach and calling push.
filter (a method that returns a new array of only the elements for which the callback returned true) handles this kind of narrowing. The function you pass receives an element and returns a boolean that says whether to keep it.
const books = [
{ title: "Practical SQL", stock: 0 },
{ title: "Networking Basics", stock: 5 },
{ title: "Linux Illustrated", stock: 2 },
];
// Write a function that returns a boolean: keep it or not
const isInStock = (book) => book.stock > 0;
// Only elements that returned true go into the new array
const available = books.filter(isInStock);
console.log(available.length); // 2
console.log(available[0].title); // Networking Basics
console.log(books.length); // 3
true go into available. The length drops by however many were left out, and the remaining elements shift forward to fill the gaps.available only knows the new positions; the index each item had in books is lost. You'll see how to work with the original position at the end of this article. The table below sums up how each of the three methods uses the return value of the function you pass.
| Method | Callback's return value | Length of the resulting array |
|---|---|---|
| map | Goes into the new array | Same as the original |
| filter | If true, the element is kept | Same as the original or fewer |
| forEach | Ignored | No array is created (returns undefined) |
Curly Braces Need a return
If you write the body in curly braces, as in (book) => { book.stock > 0 }, the return value is undefined unless you write return. undefined is falsy, so no element is kept and you get an empty array. For a one-line check, leave the curly braces off.
Narrow Down, Then Transform — Chaining filter and map
Say you want to turn only the published courses into strings for display. You could store filter's result in a variable and call .map(...) on it in a second line, but then you're naming an intermediate array you'll never use again.
filter returns an array too, so you can call .map(...) directly on its return value. Linking method calls one after another like this is called chaining (calling the next method on the previous method's return value).
const courses = [
{ title: "Intro to Git", minutes: 45, published: true },
{ title: "SQL Practice Drills", minutes: 90, published: false },
{ title: "Linux Basics", minutes: 60, published: true },
];
// Keep only the published ones, then turn the result into display strings
// A leading . calls the method on the previous line's result (it's all still one expression)
const labels = courses
.filter((course) => course.published) // published is already a boolean, so return it as-is
.map((course) => `${course.title} (${course.minutes} min)`);
console.log(labels.join(" / "));
// Intro to Git (45 min) / Linux Basics (60 min)
console.log(labels.length); // 2
console.log(courses.length); // 3
.map reads the array returned by filter without that array ever getting a name. Each step in the chain creates one new array.To inspect the intermediate result, store filter's result in a variable, print it with console.log, and then call .map(...) on that variable. Once you're done checking, you can turn it back into a chain.
Getting the Position — The Callback's index
Say you want to add ranks like "#1" and "#2" to a ranking list. The elements themselves don't store a rank, so it's tempting to keep a counter variable outside the callback and add 1 to it on every call.
map and filter pass the element as the callback's first argument, and that element's position (its index, counted from 0) as the second. Arguments are matched by position, so even if you only need the index, you still have to write the first parameter. Name that second parameter index, and you can use it both to display a rank and to check whether an element is among the first few.
const ranking = ["Wireless Earbuds", "Power Bank", "Phone Stand", "USB Hub"];
// The second argument is the position, counted from 0
const lines = ranking.map((item, index) => `#${index + 1}: ${item}`);
console.log(lines.join("\n"));
// #1: Wireless Earbuds
// #2: Power Bank
// #3: Phone Stand
// #4: USB Hub
// filter's callback receives the same two arguments
const topThree = ranking.filter((item, index) => index < 3);
console.log(topThree.length); // 3
The index passed to filter is the position in the original array. The kept elements shifting forward afterward doesn't affect the check, so index < 3 means the first three items of the original array.
Knowledge Check
Answer each question one by one.
Q2In courses.filter(isPublished).map(toLabel), which array does map read?
Q3In the callback of ranking.map((item, index) => ...), what is passed to index?