Module 2 · Data Types & Control Flow Deep Dive ⏱ 21 min

Array Methods: map, filter & reduce

By the end of this lesson you will be able to:
  • Transform every element with map
  • Select matching elements with filter
  • Aggregate an array to one value with reduce
  • Chain methods into readable data pipelines

Real programs process collections: turn every price into a formatted string, keep only the active users, or add every item to a running total. You could write a for loop for each of these, but three higher-order methods handle most collection work more cleanly.

A higher-order function is simply a function that takes another function as an argument. map, filter, and reduce are higher-order methods on arrays, and each calls the function you give it once per element. Learning to read and write these fluently is what separates beginner JavaScript from professional JavaScript.

Why prefer them over loops? Three reasons:

  • Intent is visiblefilter(isActive) tells you the goal; a loop makes you read the body to find out.
  • Immutability — every method returns a new array (or value), leaving the original untouched.
  • Composability — you can chain them into pipelines that read left-to-right like a sentence.
flowchart LR
  A["input array"] --> M["map: transform"]
  M --> F["filter: select"]
  F --> R["reduce: one value"]
  style R fill:#e0900b,color:#fff
A common pipeline: map to transform, filter to select, reduce to summarize.

map: transform every element

map builds a new array by running your function on every item. The original array is untouched, and the result always has the same length as the input — no items are added or removed, only changed:

const nums = [1, 2, 3];
const doubled = nums.map((n) => n * 2);
// doubled is [2, 4, 6]; nums is still [1, 2, 3]

The callback receives three arguments: the current item, its index, and the whole array. Most of the time you only need the item, but the index is handy for numbering or looking at neighbours. Use map whenever you have an array of things and need an array of different things in the same order.

A common mistake is using map when you want a side effect. If you call map and ignore the returned array, you probably wanted forEach — or you have a design smell. map is for transformation; if you are not using the result, do not use map.

map transforms every item into a new array of the same length.
const prices = [10, 25, 7];
const withTax = prices.map((p) => p * 1.2);
const labels = prices.map((p, i) => `Item ${i + 1}: $${p}`);
console.log(withTax);
console.log(labels);

filter: keep what passes the test

filter builds a new array containing only the items where your function returns a truthy value. The result is usually shorter than the input, but it can also be empty or identical — it depends on the data:

const nums = [1, 2, 3, 4];
const big = nums.filter((n) => n > 2);  // [3, 4]
const evens = nums.filter((n) => n % 2 === 0);  // [2, 4]

The function you pass is called a predicate — it answers true or false for each item. A common mistake is forgetting that filter does not modify the items; it only decides which ones to keep. If you need to change them too, chain map after filter.

Another trap: returning a non-boolean. filter keeps anything truthy, so nums.filter((n) => n % 2) keeps odd numbers because 1 is truthy. Be explicit — write n % 2 === 0 rather than relying on truthiness — so the next reader knows you meant a boolean test.

flowchart LR
  A["[1, 2, 3, 4]"] --> B{"n > 2 ?"}
  B -->|"true"| K1["3"]
  B -->|"true"| K2["4"]
  B -->|"false"| D1["1"]
  B -->|"false"| D2["2"]
  K1 --> R["[3, 4]"]
  K2 --> R
  style R fill:#e0900b,color:#fff
filter runs a predicate on each item and keeps only those that return true.

reduce: boil down to one value

reduce is the most flexible of the three. It walks the array and folds each item into an accumulator, carrying the result forward step by step. You provide a callback (accumulator, item) and a starting value for the accumulator:

const nums = [1, 2, 3, 4];
const sum = nums.reduce((acc, n) => acc + n, 0);  // 10

On the first step acc is 0 and n is 1, giving 1. On the second step acc is 1 and n is 2, giving 3, and so on. The starting value matters: pass 0 for sums, 1 for products, "" for string concatenation, or [] for building a new array.

Because reduce is so flexible, it is also the hardest to read. If you find yourself writing a multi-line reducer, ask whether map or filter would express the same idea more clearly. A pipeline of simple steps is usually better than one clever reducer.

filter selects items, then reduce aggregates them.
const nums = [1, 2, 3, 4, 5, 6];
const evens = nums.filter((n) => n % 2 === 0);
const sum = evens.reduce((a, b) => a + b, 0);
console.log(evens);
console.log(sum);
flowchart LR
  A["acc=0, n=1"] --> B["acc=1"]
  B --> C["acc=1, n=2"]
  C --> D["acc=3"]
  D --> E["acc=3, n=3"]
  E --> F["acc=6"]
  F --> G["acc=6, n=4"]
  G --> H["acc=10"]
  style H fill:#e0900b,color:#fff
reduce walks left-to-right, folding each element into the accumulator.

Chaining into pipelines

The real power of these methods appears when you chain them. Because each returns a new array (or single value), you can attach the next method directly:

const result = orders
  .filter((o) => o.active)
  .map((o) => o.total)
  .reduce((sum, t) => sum + t, 0);

Read this left to right: keep active orders, extract their totals, add them up. A single reduce could do the same job, but the pipeline is easier to debug — you can inspect the array after filter or after map — and easier to modify later.

One practical note: if a chain gets long, consider giving intermediate steps names. A named variable is a place to set a breakpoint, and its name documents what the step produced.

Chain filter, map, and reduce into a readable pipeline.
const scores = [45, 82, 67, 91, 55];
const passed = scores.filter((s) => s >= 60);
const curved = passed.map((s) => Math.min(s + 5, 100));
const avg = curved.reduce((a, b) => a + b, 0) / curved.length;
console.log(passed);
console.log(curved);
console.log(avg);
Exercise

What does this print? filter keeps only the items where the test returns true, and returns them as a new array.

const nums = [1, 2, 3, 4];
console.log(nums.filter((n) => n > 2));
Exercise

Write sumEven(nums) that returns the sum of the even numbers in nums. Chain filter (to keep evens) and reduce (to add them up).

function sumEven(nums) {
  // return the sum of the even numbers
}
Exercise

Predict the output. Notice the starting value of reduce and how the accumulator builds up.

const words = ["a", "bb", "ccc"];
const result = words.reduce((acc, w) => acc + w.length, 0);
console.log(result);
Exercise

Write averagePositive(nums) that returns the average of all positive numbers in nums. If there are no positive numbers, return 0. Use filter and reduce.

function averagePositive(nums) {
  // return the average of positive numbers
}
Exercise

Write productOfPositive(nums) that returns the product of all positive numbers in nums. If there are no positive numbers, return 1. Use filter and reduce. So productOfPositive([2, -3, 4]) returns 8.

function productOfPositive(nums) {
  // return the product of positive numbers, or 1 if none
}

Recap

  • map transforms every element and preserves length — use it when you need the same number of items, changed.
  • filter selects items with a predicate — use it when you need fewer items.
  • reduce folds the array into a single value — use it for sums, products, or any accumulation.
  • Always pass a starting value to reduce; omitting it breaks on empty arrays.
  • These methods chain naturally: arr.filter(...).map(...).reduce(...).
  • Prefer a pipeline of simple steps over one clever reducer; readability beats brevity.

Next you'll learn how to pass functions around as values and lock private state inside closures.

Checkpoint quiz

Which method builds a NEW array by transforming every element?

What does [1, 2, 3].reduce((a, b) => a + b, 0) return?

Go deeper — technical resources