JavaScript and TypeScript course Β· Module 12: Functional Programming

Higher-Order Functions: map, filter, reduce

7 min read
In this lesson5

The morning briefing needs three summaries at once: a list of the names of all specimens, a register of sick carnivores and the total number of dinosaurs in the park. Three for loops mean three counters, three temporary arrays and three chances for an off-by-one mistake. It can be done more briefly, and without touching the source data.

In the laboratories of Jurassic Park, scientists process enormous amounts of dinosaur data every day - filtering DNA samples, transforming analysis results, and aggregating statistics. Higher-order functions are tools that let you do the same with data in JavaScript - elegantly, readably, and safely.

What is a Higher-Order Function?

A higher-order function is a function that takes another function as an argument or returns a function as a result. It's like a machine in the laboratory that accepts an instruction (function) and applies it to each DNA sample.

You already know one: the filter method from the previous lesson accepted the isCarnivore function. The instruction you pass in is called a callback - the method itself calls it for each element of the array, and you only decide what should happen to a single element.

Array.map() - Data Transformation

map creates a new array by applying a given function to each element. The original array remains untouched.

We have three specimens and need just the list of their names:

1const specimens = [
2  { name: 'Rex', weight: 8000, age: 5 },
3  { name: 'Blue', weight: 150, age: 3 },
4  { name: 'Brachio', weight: 56000, age: 12 },
5];
6
7// Transformation - extracting just the names
8const names = specimens.map((dino) => dino.name);
9// ['Rex', 'Blue', 'Brachio']

The result always has as many elements as the input array, because map does not discard anything, it only transforms. specimens still contains the full objects.

The callback can also build a completely new object. Note the parentheses around the curly braces: without them JavaScript would treat { as the start of the function body, not an object literal.

1// Transformation - computing a report
2const reports = specimens.map((dino) => ({
3  label: dino.name.toUpperCase(),
4  weightInTons: dino.weight / 1000,
5  category: dino.weight > 5000 ? 'large' : 'small',
6}));
7// [{ label: 'REX', weightInTons: 8, category: 'large' }, ...]

Each report is a new object: toUpperCase turns the name into capital letters, the weight comes out in tons, and the conditional operator ? : assigns a category. One warning: if the callback in map returns nothing, you will get an array full of undefined. For side effects alone, such as writing to the console, use forEach.

Array.filter() - Data Selection

filter creates a new array with elements that satisfy a condition (predicate). It's like filtering DNA samples - you keep only those that pass quality control.

A predicate is a function that returns true or false. Let's run a herd of four dinosaurs through two filters:

1const allDinosaurs = [
2  { name: 'Rex', diet: 'carnivore', health: 95 },
3  { name: 'Brachio', diet: 'herbivore', health: 88 },
4  { name: 'Raptor', diet: 'carnivore', health: 42 },
5  { name: 'Stego', diet: 'herbivore', health: 100 },
6];
7
8// Filtering - only healthy dinosaurs
9const healthy = allDinosaurs.filter((dino) => dino.health > 80);
10// [Rex, Brachio, Stego]
11
12// Filtering - predators needing help
13const sickCarnivores = allDinosaurs.filter(
14  (dino) => dino.diet === 'carnivore' && dino.health < 50
15);
16// [Raptor]

The first filter keeps three specimens, the second, which combines two conditions with the && operator, only Raptor. filter always returns an array, even with a single element or an empty one, never a single true: [10, 20, 30].filter((x) => x > 15) gives [20, 30]. The objects are not copied in the process - healthy[0] is the very same object as allDinosaurs[0].

Array.reduce() - Data Aggregation

reduce processes an array into a single value, accumulating the result element by element. It's like a summary report - it gathers data from all enclosures and creates one summary.

The method takes two arguments: a combining function and an initial value. The function receives the accumulator (the result so far) and the current element, and returns the new accumulator:

1const parkData = [
2  { zone: 'A', dinosaurs: 5, incidents: 0 },
3  { zone: 'B', dinosaurs: 3, incidents: 2 },
4  { zone: 'C', dinosaurs: 8, incidents: 1 },
5];
6
7// Sum of all dinosaurs
8const totalDinos = parkData.reduce(
9  (sum, zone) => sum + zone.dinosaurs, 0
10);
11// 16

The accumulator starts at 0 and then grows: 0 + 5 = 5, 5 + 3 = 8, 8 + 8 = 16. Always provide the initial value. Without it, reduce takes the first element as the accumulator (here the whole zone A object, so the result would be meaningless text), and on an empty array it throws a TypeError.

The accumulator can also be an object. Let's group the zones by safety:

1// Grouping zones by safety
2const grouped = parkData.reduce((groups, zone) => {
3  const key = zone.incidents === 0 ? 'safe' : 'risky';
4  return { ...groups, [key]: [...(groups[key] || []), zone.zone] };
5}, {});
6// { safe: ['A'], risky: ['B', 'C'] }

The [key]: notation is a computed property name: the key is taken from the value of the key variable, that is 'safe' or 'risky'. The expression groups[key] || [] returns the existing list, or an empty array when the group does not exist yet. Each step builds a new groups object instead of changing the previous one. Newer environments (ES2024) also have a ready-made Object.groupBy, but it is worth understanding what it does under the hood.

Chaining map, filter, and reduce

The true power of FP reveals itself when you chain these functions into a processing pipeline. filter and map return arrays, so we can call the next method on the result right away:

1const specimens = [
2  { id: 'S001', species: 'T-Rex', viability: 0.95, cost: 50000 },
3  { id: 'S002', species: 'Raptor', viability: 0.3, cost: 20000 },
4  { id: 'S003', species: 'Brachio', viability: 0.88, cost: 80000 },
5  { id: 'S004', species: 'Stego', viability: 0.72, cost: 35000 },
6  { id: 'S005', species: 'Raptor', viability: 0.91, cost: 22000 },
7];
8
9// Pipeline: filter viability > 0.7 -> calculate cost -> sum
10const totalViableCost = specimens
11  .filter((s) => s.viability > 0.7)
12  .map((s) => s.cost * s.viability)
13  .reduce((total, cost) => total + cost, 0);
14// 47500 + 70400 + 25200 + 20020 = 163120

The filter rejects only sample S002, map turns the remaining four into a cost weighted by viability, and reduce folds them into a single number. Order matters: filter first, transform second, so that map does not compute anything for rejected samples. And when a reduce grows to a dozen lines, split it into smaller steps - readability matters more than cleverness. In the next lesson you will turn a chain like this into a named pipeline with pipe.

Next time you stand in front of a table of specimen data, picture three machines on the bench - filter keeps the samples that meet your condition, map transforms each one, and reduce folds them all into a single report - and not one of them ever touches the original array.

Code for this lesson: index.js
1// Higher-order functions: map, filter, reduce
2console.log("=== Jurassic Park - DNA Data Processing ===\n");
3
4const specimens = [
5  { id: "S001", name: "Rex", species: "T-Rex", weight: 8000, diet: "carnivore", health: 95 },
6  { id: "S002", name: "Blue", species: "Velociraptor", weight: 150, diet: "carnivore", health: 88 },
7  { id: "S003", name: "Brachio", species: "Brachiosaurus", weight: 56000, diet: "herbivore", health: 100 },
8  { id: "S004", name: "Stego", species: "Stegosaurus", weight: 5000, diet: "herbivore", health: 72 },
9  { id: "S005", name: "Raptor", species: "Velociraptor", weight: 140, diet: "carnivore", health: 42 },
10  { id: "S006", name: "Trike", species: "Triceratops", weight: 9000, diet: "herbivore", health: 91 },
11];
12
13// MAP - data transformation
14console.log("--- map: Weight reports ---");
15const weightReports = specimens.map((s) => ({
16  name: s.name,
17  weightInTons: (s.weight / 1000).toFixed(1) + "t",
18  category: s.weight > 5000 ? "LARGE" : "SMALL",
19}));
20weightReports.forEach((r) => console.log(`  ${r.name}: ${r.weightInTons} (${r.category})`));
21
22// FILTER - data selection
23console.log("\n--- filter: Healthy predators ---");
24const healthyCarnivores = specimens.filter(
25  (s) => s.diet === "carnivore" && s.health > 80
26);
27healthyCarnivores.forEach((s) => console.log(`  ${s.name}: health=${s.health}`));
28
29console.log("\n--- filter: Dinosaurs that need treatment ---");
30const needCare = specimens.filter((s) => s.health < 80);
31needCare.forEach((s) => console.log(`  ${s.name}: health=${s.health}`));
32
33// REDUCE - aggregation
34console.log("\n--- reduce: Statistics ---");
35const totalWeight = specimens.reduce((sum, s) => sum + s.weight, 0);
36console.log("Total weight:", totalWeight, "kg");
37
38const avgHealth = specimens.reduce((sum, s) => sum + s.health, 0) / specimens.length;
39console.log("Average health:", avgHealth.toFixed(1));
40
41// Grouping with reduce
42const byDiet = specimens.reduce((groups, s) => {
43  const key = s.diet;
44  return { ...groups, [key]: [...(groups[key] || []), s.name] };
45}, {});
46console.log("By diet:", byDiet);
47
48// PIPELINE - a chain of operations
49console.log("\n--- Pipeline: Upkeep cost of healthy large dinosaurs ---");
50const maintenanceCost = specimens
51  .filter((s) => s.health > 80)         // healthy only
52  .filter((s) => s.weight > 1000)        // large only
53  .map((s) => ({ name: s.name, cost: s.weight * 2 })) // cost = 2x weight
54  .reduce((total, s) => {
55    console.log(`  ${s.name}: $${s.cost}`);
56    return total + s.cost;
57  }, 0);
58console.log("TOTAL:", "$" + maintenanceCost);

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What does the Array.map() method do in JavaScript?

  2. 2. What will [1,2,3,4,5].filter(x => x > 3) return?

Hands-on tasks in the game

  • Code editor

    Complete the functions calculateThreatIndex, classifyThreat, and createReport so that they are pure.

  • Click in order

    Arrange the elements of a reduce call that sums an array:

  • Code editor

    Use map() for name extraction, filter() for selection, reduce() for aggregation, and combine them into a pipeline.

Useful articles