map, filter & reduce
Transform lists declaratively instead of with loops: map every item, filter what stays, reduce many values into one result, chain array methods, flatten nested arrays, and choose forEach only for side effects.
- 01Choose the right array methodUse map, filter, reduce, flat, flatMap, and forEach for their intended jobs.
- 02Trace callbacks confidentlyPredict callback arguments, return values, accumulator changes, and common gotchas.
- 03Build readable data pipelinesChain small steps and inspect each real intermediate array before reducing to a final value.
Transform lists declaratively
Arrays often hold many similar things: products in a cart, names in a roster, messages in an inbox, points on a chart. You already know how a loop can visit each index. This lesson gives you the array-method vocabulary professionals use when the goal is clearer than the index mechanics.
The short version is a decision chart: use map when every item becomes one new item, filter when some items stay, reduce when many items become one final value, and forEach only when you want a side effect. flat and flatMap help when arrays are tucked inside other arrays.
Every method here takes a callback. This lesson assumes Function expressions & callbacks, Arrow functions, Object.keys/values/entries, and Loops. It also prepares you for later Stage 8 lessons titled Higher-order functions and Composition & pipelines.
| Method | Use it when | Returns | Original array |
|---|---|---|---|
map | Every item becomes one transformed item | A new array with the same length | Not changed by the method itself |
filter | Items must pass a test to stay | A new array, often shorter | Not changed by the method itself |
reduce | The whole array becomes one value | Whatever the accumulator becomes | Not changed by the method itself |
flat | Nested arrays should be unpacked | A new flatter array | Not changed by the method itself |
forEach | You need a side effect, not a result | undefined | Only changed if your callback changes something |
map: transform every item
SAME COUNTmap is the array factory conveyor belt. Every item enters the same machine, your callback runs, and one transformed item comes out. The new array has the same number of slots as the original.
Imagine notebooks on a belt. A stamping machine prints a price label on every notebook. It does not decide whether notebooks belong in the shipment; it transforms each one in the same way.
- In real life: A box enters the machine
- In JavaScript: One array item becomes the callback argument
- In real life: The machine labels or reshapes it
- In JavaScript: The callback returns the transformed value
- In real life: One box leaves for every box that entered
- In JavaScript: The returned array has the same length
Where the analogy stops: A real factory can reject broken boxes. map does not remove items; use filter when the count may shrink.
These examples prove several map facts at once: braces arrows need an explicit return, returning object literals needs parentheses, map(parseInt) is a classic trap, and the original array is not mutated.
const numbers = [1, 2, 3];const missingReturn = numbers.map(n => { n * 2; });const objects = numbers.map(n => ({ value: n }));const parsed = ["1", "2", "3"].map(parseInt); console.log(missingReturn);console.log(objects);console.log(parsed);console.log(numbers);- Line 2 prints
[undefined, undefined, undefined]because a braces arrow body must sayreturn. - Line 3 works because
({ value: n })means “return this object.” Without parentheses, JavaScript reads braces as a function body. - Line 4 prints
[1, NaN, NaN].mappasses(value, index, array), andparseInttreats the index as its radix argument.
The last log still prints [1, 2, 3]. The method creates a new array. Your callback can still mutate objects inside the array, so keep callbacks pure unless mutation is the point.
filter: keep passing items
TRUTHY TESTfilter is a sieve or a bouncer. The callback returns a value for each item. If that value is truthy, the item gets through; if it is falsy, the item is left out. The callback does not need to return the literal boolean true, but comparisons like age >= 18 are nice and clear.
Filtering is not transformation. The original item that passes the test is copied into the new array. If you want to change the item after keeping it, chain filter and map.
- In real life: A sieve lets small grains pass and blocks big pieces
- In JavaScript: The callback's truthy/falsy result decides what stays
- In real life: A bouncer checks an ID
- In JavaScript: A condition like
person.age >= 18 - In real life: The line outside may shrink
- In JavaScript: The returned array may be shorter or empty
Where the analogy stops: A real bouncer can explain the rule. JavaScript only uses the value your callback returns, so write readable tests.
const values = [0, "hi", "", 42, null, "ok"];const compact = values.filter(Boolean);const adults = [{ age: 17 }, { age: 22 }, { age: 31 }].filter(person => person.age >= 18); console.log(compact);console.log(adults.map(person => person.age));filter(Boolean) is a compact idiom for dropping falsy values such as 0, "", null, undefined, and NaN. Use it when “remove empty values” is really what you mean; do not use it when 0 is a valid value you need to keep.
reduce & reduceRight: fold many values into one
STEP THROUGHreduce is the most powerful and the easiest to overuse. Its callback receives an accumulator and the current item. Whatever the callback returns becomes the accumulator for the next item. At the end, reduce returns the accumulator.
The accumulator is the snowball. A sum starts at 0 and gathers each price. A word counter starts as {} and gathers properties. A string builder starts as "" and gathers text.
- In real life: A snowball starts small
- In JavaScript: The initial accumulator, like
0or{} - In real life: It rolls over each patch of snow
- In JavaScript: Each array item reaches the callback
- In real life: It ends as one bigger ball
- In JavaScript: The final total, object, string, or array
Where the analogy stops: A snowball always gets bigger. A reducer can also replace, shrink, or reshape the accumulator; the callback return decides.
Choose a reduce setup, then step through what the accumulator does.
script
const total = prices.reduce((acc, price) => { return acc + price;}, 0);console.log(total);Prefer an explicit initial value. Omitting it makes the first array item the starting accumulator, so callbacks begin at the second item. On an empty array, [].reduce(callback) throws TypeError: Reduce of empty array with no initial value.
reduceRight uses the same accumulator idea but visits indexes from right to left. That matters when order matters: composing functions right-to-left, rebuilding text from the end, or explaining a right-associative operation.
Chaining methods: build a readable pipeline
INTERACTIVEArray methods return values, so you can put the next method directly after the previous one. A chain is an assembly line of small machines: filter the cart, map tax onto each item, filter by price, then reduce to one total.
A good chain reads like a checklist. If one step gets complicated, split it into a named helper or a named intermediate array before continuing.
- In real life: Machine 1 removes broken parts
- In JavaScript:
.filter(item => item.inStock) - In real life: Machine 2 adds a label
- In JavaScript:
.map(item => ({ ...item, priceWithTax })) - In real life: Machine 3 boxes the final order
- In JavaScript:
.reduce((sum, item) => ...)
Where the analogy stops: Real assembly lines hide intermediate states. Good code often names a temporary variable or uses a playground like this one when a chain gets hard to read.
const cart = [ { name: "Notebook", price: 4, inStock: true }, { name: "Marker", price: 2, inStock: false }, { name: "Mug", price: 8, inStock: true }, { name: "Stickers", price: 3, inStock: true },]; const total = cart .filter(item => item.inStock) .map(item => ({ ...item, priceWithTax: item.price * 1.1 })) .filter(item => item.priceWithTax > 5) .reduce((sum, item) => sum + item.priceWithTax, 0);- start
[ { "name": "Notebook", "price": 4, "inStock": true }, { "name": "Marker", "price": 2, "inStock": false }, { "name": "Mug", "price": 8, "inStock": true }, { "name": "Stickers", "price": 3, "inStock": true } ] - filter in-stock
[ { "name": "Notebook", "price": 4, "inStock": true }, { "name": "Mug", "price": 8, "inStock": true }, { "name": "Stickers", "price": 3, "inStock": true } ] - map tax
[ { "name": "Notebook", "price": 4, "inStock": true, "priceWithTax": 4.4 }, { "name": "Mug", "price": 8, "inStock": true, "priceWithTax": 8.8 }, { "name": "Stickers", "price": 3, "inStock": true, "priceWithTax": 3.3 } ] - filter > 5
[ { "name": "Mug", "price": 8, "inStock": true, "priceWithTax": 8.8 } ] - reduce total
8.8
The chain ends in one number: 8.8. That is reduce's job.
Ordinary arrays are eager: filter and map create real intermediate arrays immediately. That is clear and usually fine. Later, Higher-order functions and Composition & pipelines revisit ways to build reusable or lazy-looking flows.
flat & flatMap: unpack boxes inside boxes
Sometimes the result of a transformation is nested: arrays inside an array. flat returns a new array with nested arrays unpacked by a chosen depth. With no argument it unwraps one level. flatMap maps each item and then flattens one level.
const nested = [1, [2, [3]]];const oneLevel = nested.flat();const allLevels = nested.flat(Infinity);const letters = ["map", "fun"].flatMap(word => word.split("")); console.log(oneLevel);console.log(allLevels);console.log(letters);[1, [2, [3]]].flat() produces [1, 2, [3]], because the inner [3] is still one level too deep. Use flat(Infinity) when you intentionally want all levels. flatMap(word => word.split("")) is a compact way to turn words into one stream of letters.
forEach vs map: side effects vs returned arrays
SORTERThe biggest beginner confusion is using forEach when map is needed. forEach calls your callback for each item and returns undefined. It is for side effects: logging, updating the page, pushing into an existing array, or calling a function that changes something outside the callback.
const numbers = [1, 2, 3];const mapped = numbers.map(n => n * 2);const visited = [];const eachResult = numbers.forEach(n => visited.push(n * 2)); console.log(mapped);console.log(visited);console.log(eachResult);- Turn
[2, 4, 6]into[4, 8, 12] - Turn user objects into an array of display names
- Keep only products where
inStockis true - Keep people with
age >= 18 - Turn cart items into one total price
- Count how many times each word appears
- Print every item for debugging
- Add a class to each existing element on a page
Sort each task by the method that matches the shape of the result. Same-length array, shorter array, one value, or side effect?
One more difference: forEach has no normal break. If you need to stop early, use for...of or a search method like some or every.
Where you’ll use this
Array methods show up anywhere data becomes UI. Object.keys/values/entries often pair with map: turn an object into entries, transform the entries, then render or rebuild. Here are reducer patterns beyond sums: counts and groups.
const words = ["tea", "coffee", "tea", "juice"];const counts = words.reduce((result, word) => { result[word] = (result[word] ?? 0) + 1; return result;}, {}); const grouped = words.reduce((result, word) => { const first = word[0]; (result[first] ??= []).push(word); return result;}, {}); console.log(counts);console.log(grouped);Grouping is common enough that newer JavaScript has Object.groupBy in supporting environments. Feature-check it with typeof Object.groupBy === "function" before relying on it in shared code. reduce remains useful when you need a custom shape or broader compatibility.
Common misconceptions
“map changes the original array.”
map returns a new array. The original array reference is untouched, although objects inside it can still be mutated by a careless callback.
“filter callbacks must return true or false.”
They may return any value. JavaScript keeps the item when the value is truthy. Clear boolean expressions are still easiest to read.
“reduce is only for sums.”
Sums are the first example, not the whole tool. Reduce can build objects, groups, indexes, strings, maximum values, and flattened arrays.
“forEach is just map without a return.”
That missing return is the point. forEach is for side effects and gives you undefined. If you need the transformed values, use map.
“A long chain is always cleaner than a loop.”
Chains are great when each step is tiny and named by the method. If the callbacks get complicated, split the work or use a loop.
Practice: choose and use the method
6 EXERCISESRun the code. What array prints?
const numbers = [2, 4, 6];
const doubled = numbers.map(number => number * 2);
console.log(doubled);const numbers = [2, 4, 6];
const doubled = numbers.map(number => number * 2);
console.log(doubled);Each number returns its doubled value, so map builds [4, 8, 12].
Which names are left after filtering for adults?
const people = [
{ name: "Ari", age: 17 },
{ name: "Bea", age: 22 },
{ name: "Cal", age: 31 },
];
const adults = people.filter(person => person.age >= 18);
console.log(adults.map(person => person.name).join(", "));const people = [
{ name: "Ari", age: 17 },
{ name: "Bea", age: 22 },
{ name: "Cal", age: 31 },
];
const adults = people.filter(person => person.age >= 18);
console.log(adults.map(person => person.name).join(", "));Ari is 17, so the filter removes Ari. Bea and Cal stay.
Run the reducer and type the total that prints.
const cart = [
{ name: "Pencil", price: 1.5 },
{ name: "Notebook", price: 4 },
{ name: "Mug", price: 8 },
];
const total = cart.reduce((sum, item) => sum + item.price, 0);
console.log(total);const cart = [
{ name: "Pencil", price: 1.5 },
{ name: "Notebook", price: 4 },
{ name: "Mug", price: 8 },
];
const total = cart.reduce((sum, item) => sum + item.price, 0);
console.log(total);The accumulator starts at 0, then becomes 1.5, 5.5, and finally 13.5.
How many times does the word map appear?
const words = ["map", "filter", "map", "reduce", "map"];
const counts = words.reduce((result, word) => {
result[word] = (result[word] ?? 0) + 1;
return result;
}, {});
console.log(counts.map);const words = ["map", "filter", "map", "reduce", "map"];
const counts = words.reduce((result, word) => {
result[word] = (result[word] ?? 0) + 1;
return result;
}, {});
console.log(counts.map);The word map appears three times, so counts.map is 3.
The starter prints [undefined, undefined, undefined]. Fix it. What prints after the fix?
const numbers = [1, 2, 3];
const doubled = numbers.map(number => {
number * 2;
});
console.log(doubled);const numbers = [1, 2, 3];
const doubled = numbers.map(number => {
return number * 2;
});
console.log(doubled);Adding return makes each callback result become an entry in the mapped array.
Predict this famous output before you run it.
console.log(["1", "2", "3"].map(parseInt));console.log(["1", "2", "3"].map(parseInt));The calls are parseInt("1", 0), parseInt("2", 1), and parseInt("3", 2), so the result is [1, NaN, NaN].
Quiz: check your understanding
8 QUESTIONSRead each callback carefully. Your first-try score matters, but every answer explanation is part of the lesson.
Question 1 of 8Which method should you reach for when every input item should produce one transformed output item?
Choose an answer to see the explanation.
Question 2 of 8What does the missing-return map example print?
Read the code, then predictconsole.log([1, 2, 3].map(n => { n * 2; }));Choose an answer to see the explanation.
Question 3 of 8What does the parseInt map example print?
Read the code, then predictconsole.log(["1", "2", "3"].map(parseInt));Choose an answer to see the explanation.
Question 4 of 8What does filter(Boolean) print here?
Read the code, then predictconsole.log([0, "hi", "", 42, null].filter(Boolean));Choose an answer to see the explanation.
Question 5 of 8What total does this reduce print?
Read the code, then predictconsole.log([5, 12, 3].reduce((acc, price) => acc + price, 0));Choose an answer to see the explanation.
Question 6 of 8Which statement about
reduceRightis true?Choose an answer to see the explanation.
Question 7 of 8What does one-level flat print here?
Read the code, then predictconsole.log([1, [2, [3]]].flat());Choose an answer to see the explanation.
Question 8 of 8What does
forEachreturn in this code?Read the code, then predictconst result = [1, 2, 3].forEach(n => n * 2); console.log(result);Choose an answer to see the explanation.
Key takeaways
maptransforms each item and returns a same-length array.filterkeeps items whose callback result is truthy.reduceuses an accumulator to fold many items into one final value; give it an initial value.flatunpacks nested arrays;flatMapmaps and flattens one level.forEachreturnsundefinedand belongs to side effects, not transformations.
One-liner.
Use map to transform, filter to keep, reduce to fold, flat to unpack, and forEach only to do something on the side.
Up next: Sorting & copying safely.