Learn by reading through in order

reduce — Folding an Array Into a Single Value

Learn reduce, which combines an array into one value. Covers the accumulator and initial value, sums, maximums, grouping, and the TypeError on empty arrays.

Say you need the total for an order's line items. map keeps the same number of elements and filter only removes some, so either way you get an array back. Neither one can combine an array into a single value like a total or a maximum.

This article covers reduce, which combines an array into one value, along with the initial value it starts from.

Combining an Array Into One Value — reduce and the Initial Value

Say you want to show a shopping cart's total on screen. With forEach, the variable holding the total has to live outside the loop, where any later line could overwrite it. And since its value changes with every addition, you have to declare it with let instead of const.

The array method reduce (a method that passes the elements to a callback one at a time and returns only the last return value) takes a callback whose first parameter is the accumulator (the parameter that holds the value returned by the previous call). Unlike with map and filter, the element comes second. The second argument to reduce itself is the initial value (the value the accumulator holds on the first call).

const cartItems = [
  { name: "Laptop Stand", price: 3200 },
  { name: "USB Hub", price: 2480 },
  { name: "HDMI Cable", price: 1180 },
];

// The first parameter is the previous call's return value; the second is the current element
const addPrice = (total, item) => total + item.price;

// The second argument, 0, becomes total on the first call
const totalPrice = cartItems.reduce(addPrice, 0);
console.log(totalPrice);          // 6860

// What comes back is a single value, not an array
console.log(typeof totalPrice);   // number
total Is Replaced on Every Call
Call 1:total is 0Returns0 + 3200Call 2:total is 3200Returns3200 + 2480Call 3:total is 5680Returns5680 + 1180totalPriceis 6860Last return valueis stored as-is
The 3200 returned by call 1 is passed in as total on call 2. The 6860 returned by the last call is reduce's result.

You don't need your own variable for the running total, because reduce takes care of passing the value from one call to the next. And what gets passed along doesn't have to be a number; it can be an element or an object, too.

From an office supplies order list, work out the total quantity and cost of the whole order. orderLines is already declared, with one product per line.

① Display the total quantity.

② Display the total cost, where each line's cost is unit price × quantity.

③ Display the average cost per product, which is ②'s total divided by the number of products.

(If it runs correctly, an explanation will appear.)

JavaScript / TypeScript Editor

Run code to see output

Keeping the Larger One — Carrying a Number or an Element

Say you want to know which of your uploaded videos has the most views. This time you don't add anything up; you compare values and carry only the larger one into the next call. The question is what the accumulator should start as.

The accumulator can hold a number or the element itself. Pass 0 as the second argument and a number is carried from call to call; pass videos[0] and the element itself is carried. Make the initial value the same kind of value the callback returns.

const videos = [
  { title: "SQL Basics #1", views: 8200 },
  { title: "Linux Basics #3", views: 15400 },
  { title: "Git Fundamentals", views: 9700 },
];

// The initial value is a number, so it compares and returns numbers
const maxViews = videos.reduce((max, video) => Math.max(max, video.views), 0);
console.log(maxViews);         // 15400

// The initial value is an element, so it compares and returns elements
const topVideo = videos.reduce(
  (top, video) => (video.views > top.views ? video : top),
  videos[0],
);
console.log(topVideo.title);   // Linux Basics #3
Whatever You Start With Is Carried to the End
Initial value 0(a number)Compare maxwith video.viewsReturn thelarger numbermaxViews is15400Initial valuevideos[0]Compare top.viewswith video.viewsReturn thelarger elementtopVideo.titleis available
The top row carries only a number, while the bottom row carries the element as-is. When you also need the title, use an element as the initial value.

Both calls to reduce compare views; the only difference is what gets passed along. If you gave the second one an initial value of 0, top.views would be undefined on the first call, so the comparison would be false every time and 0 would be returned all the way to the end.

An Initial Value of 0 Breaks With Negative Numbers

In an array that can contain negative values, like temperature readings, starting from 0 means 0 stays the larger value even when the real maximum is -3, so you get a result that isn't in the array. Compare the elements against each other, starting from the array's first element, and the range of the values no longer matters.

Using this month's sales and costs for each store, find the stores with the highest and lowest profit. stores is already declared, and amounts are in units of 10,000 yen.

① Display the name of the store with the highest profit (sales − cost).

② Display the name of the store with the lowest profit.

③ Display the profit of ②'s store.

④ Display the difference in profit between ①'s store and ②'s store.

JavaScript / TypeScript Editor

Run code to see output

Folding Counts Into an Object — Grouping

Say you want to know how many support tickets there are in each category. Unlike a total, the result isn't a single number; you need one value per category. You could create an empty object outside the loop and add to it with forEach, but then the tally is once again left sitting outside the operation.

You can use {} as the initial value too. Here the accumulator is the object being tallied, and the callback updates a key and then returns that object. Reading a key that hasn't been added yet gives undefined, so add ?? 0 to start the count at 0.

const tickets = [
  { subject: "I'd like to return an item", category: "Returns" },
  { subject: "My order hasn't arrived", category: "Shipping" },
  { subject: "Please reissue my shipping label", category: "Shipping" },
];

// Add 1 to the count for each ticket's category
const countByCategory = tickets.reduce((counts, ticket) => {
  counts[ticket.category] = (counts[ticket.category] ?? 0) + 1;
  return counts;          // Pass the updated counts to the next call
}, {});                   // This {} becomes counts on the first call

console.log(JSON.stringify(countByCategory));  // {"Returns":1,"Shipping":2}
console.log(countByCategory["Shipping"]);      // 2
All Three Calls Update the Same counts Object
Call 1:counts is {}Set Returns to 1and return itCall 2:Returns is 1Set Shipping to 1and return itCall 3: Returns 1and Shipping 1Set Shipping to 2and return it
Every call adds 1 to a key on the counts it receives and returns that same object. The only object written to is the one passed as the initial value.

The initial {} is a new object that holds only the tally, so nothing is written to the elements of tickets. The table below sums up the initial value and the callback's return value for the three uses covered so far.

What you fold intoInitial valueWhat the callback returns
A total0The previous total plus the value
The largest elementThe array's first elementWhichever element is larger
Counts per categoryAn empty objectThe object with an updated key

Total up an online shop's sales by payment method. orders is already declared, and amount is the value of a single order.

① Create an object salesByMethod with payment methods as keys and their total amounts as values.

② Convert salesByMethod to a JSON string and display it.

③ Display the credit card share of total sales as a percentage (just the number).

④ Display how many different payment methods there were.

JavaScript / TypeScript Editor

Run code to see output

When There Are No Elements — reduce Without an Initial Value

A filtered result can end up empty. If you total the day's shipments on a day with no shipments at all, reduce either returns 0 or throws an error, depending on how you wrote it.

You can leave out the second argument, the initial value. If you do, the array's first element becomes the starting accumulator, and the first call to the callback gets the element at index 1. An empty array has no first element to take, so a TypeError is thrown.

const yesterdayAmounts = [3200, 2480, 1180];
const todayAmounts = [];

// Without an initial value, the first element, 3200, becomes the starting total
console.log(yesterdayAmounts.reduce((total, amount) => total + amount));  // 6860

// Even on an empty array, an initial value is returned as-is
console.log(todayAmounts.reduce((total, amount) => total + amount, 0));   // 0

// Without an initial value, there's no way to decide the starting total
// todayAmounts.reduce((total, amount) => total + amount);
// TypeError: Reduce of empty array with no initial value
An Empty Array With and Without an Initial Value
Empty array,initial value 0No elementsto passThe callbackisn't calledThe initial 0is returned as-isEmpty array,no initial valueNo first elementto takeNo startingtotalThrows aTypeError
The same empty array gives different results depending on whether you pass an initial value. The first element is required only when there's no initial value.

This kind of bug is easy to miss when you write the code: it works with the data you have, and only breaks on the first day when nothing matches. When you pass a filtered result to reduce, pass an initial value in case it's empty.

Leave Out {} and the First Element Gets Written To

If you leave out the final {} in the countByCategory example, the ticket object tickets[0] itself becomes the starting counts. No error is thrown: a Shipping key gets added to that object, and the one Returns ticket is never counted. Even when the array isn't empty, always pass an initial value when folding into an object.

Total up the large refunds in this month's refund records. refunds is already declared, and amounts are in yen.

① Display the total of all refunds, leaving out the initial value.

② Create an array with only the refunds of 5000 yen or more, and display how many there are.

③ Display the total of ②'s array, passing an initial value.

④ Display the total of ②'s array, leaving out the initial value.

JavaScript / TypeScript Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1What is passed as the first argument to the callback you give reduce?

Q2If you pass an initial value of 0 to a reduce that compares the elements' views and returns the larger one, what comes back?

Q3What happens when you call reduce without an initial value on an empty array?