Functional Style TypeScript 3: Array.reduce

Lesson 3: Array.reduce

Overview

So far, we've seen Array.map and Array.filter. These allowed us to apply some logic (via an arrow function) independently to each element in an array. Sometimes we want to maintain a running total of applying some calculation to every element instead. A basic example would be summing all numbers in an array, however, we may want to build up a more complex result, such as an object with several keys and values. This is where reduce comes in handy.

Scenario

Apply reduce to build a single value (possibly an array or object) by performing a calculate and combine step on every element in an existing array.

Example 1: Running Total

reduce is more complex than map or filter, so let's start slow. Suppose we have a number[] called playerPoints that holds the number of points scored by each player on the team. We want to calculate the total number of points scored by the team by adding up the points scored by each player.

const playerPoints: number[] = [10, 8, 3, 4, 15, 3, 0, 12, 7, 0, 0];

const teamPoints = playerPoints.reduce(
    (runningTotal: number, nextPlayersPoints: number) => runningTotal + nextPlayersPoints,
    0
);
Example 1

In example 1, the callback provided to reduce  takes 2 parameters (a third for the index is also available). The first parameter is called the accumulator, and is often named acc or accum or accumulator, although I prefer to be more declarative in my own code. The accumulator maintains the running total, in this case, when processing the ith element, runningTotal will hold the total points scored by the players indexed by 0..i-1.

The second parameter is a placeholder for each element in the array. So when we are on the value 15 (index 4), runningTotal will have the value 25 (=10+8+3+4) and nextPlayerPoints the value 15.

reduce takes a second parameter after the callback. The actual parameter (argument) we pass in this placeholder in the example is 0. This parameter will be used as the initial value for the accumulator. So we start our sum at 0 in this example.

Example 2: An Object as the Accumulator

Suppose we have a number[] called integers that we would like to split into two arrays: one called evens to hold the even integers from the original list, and another called odds to hold the odd numbers.

We could use Array.filtertwice. But there's no need to check isEven and isOdd for every element. Another way to do this is to use reduce and build up an object that has two number[] valued properties as follows.

type EvensAndOdds = {
    evens: number[];
    odds: number[];
}

const isEven = (n: number) => n % 2 === 0;

const integers: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const evensAndOdds = integers.reduce(
    ({ evens, odds }: EvensAndOdds, nextInteger) => isEven(nextInteger)
        ? {
            evens: evens.concat(nextInteger),
            odds
        }
        : {
            evens,
            odds: odds.concat(nextInteger)
        },
    {
        evens: [],
        odds: []
    }
);
Example 2

There is a lot to unpack here, so don't feel overwhelmed. Let's start with integers.reduce(...). The first parameter is an object of type EvensAndOdds. We are  using the destructuring syntax in line, so at each step we will have two number[] valued variables local to the execution context of the arrow function, called evens and odds.

The second parameter is simply the next integer in the list, and is aptly named nextInteger.

At each pass we return an updated accumulator by reusing the existing value of one of the properties ( evenor odd ) and leaving the other unchanged. We decide which is the case using the predicate function isEven and then we use an implicit return along with a ternary expression for the conditional logic to avoid the need for a code block and return keyword.

Alternatives

I could be persuaded to prefer the following code. It loops twice, but O(2n) is O(n) and this code is presumably running in memory, so readability might lead us to prefer the following.

const evens = integers.filter(isEven);

const odds = integers.filter(isOdd);

Either way, Example 2 gives you an example of how to build up an object while reducing that doesn't require any deep domain knowledge. You will surely have occasion to use reduce in this way in a real codebase.

Also note that we didn't have to represent this data as an object. We could also (gone Pythonic and) have chosen a tuple of type [String[],String[]]. The main point is that reduce allows you to build up a single accumulated value, but this value may itself be a collection.

One method to rule them all

And when you think about it, this fact makes reduce general enough to do anything that can be done with map, filter, and friends.

For example, we could implement filter as follows. Viewer warning – If currying and recursion make you uneasy, feel free to take my word for it and move to the next section.

const Filterable = <T>(valueToLift: T[]) => ({
    value: valueToLift,
    filter: (
    	predicateFunction: (input: T) => boolean
    ) => {
    	const newArray = valueToLift.reduce(
        	(keptValues: T[],element: T) => predicateFunction(element) ? keptValues.concat(element) : keptValues,
            []
        );
        
        return Filterable(newArray);
    },
    toArray: () => valueToLift
})

const BIG_NUMBER_BY_OUR_STANDARDS = 100;

const isBig = (n: number) => n >= BIG_NUMBER_BY_OUR_STANDARDS;

const isEven = (n: number) => n%2 === 0;

// Build an array with integers in order from `0..199`
const integers = Array(BIG_NUMBER_BY_OUR_STANDARDS*2)
.fill('')
.map(
    (_,index) => index
);

const bigEvens = Filterable(integers).filter(isBig).filter(isEven).toArray();

// Logs 100,102,104,...,198
bigEvens.forEach(
    (number) => console.log(number)
);
Example 3

I'm not suggesting that you actually write code as in Example 3, nor that you neglect map, filter, flatMap, some, any, and friends when they are aligned with your use case. I have had a good chuckle at my own expense when realizing I can refactor a reduce to leverage one of these instead. But I just want to point out the general utility of reduce.

Relationship to Redux

Redux is a common state management library for the client. Although commonly associated with React, it is written in pure JavaScript and can be used anywhere you have a JavaScript runtime. If you understand the way reduce works in JavaScript, you'll have no trouble understanding the pattern behind Redux.

Redux maintains a single object in-memory that is the source of truth for state. This state object is essentially a global accumulator. State is updated by dispatching an action, which is an object that has a type and payload and can be thought of as an event that has changed the state in some isolated way. In our analogy, the action is like an element in an array.

Redux leverages a reducer that takes in the existing state and the next piece of information and calculates the new state. Sound familiar?

Immutability

reduce does not mutate the original array. However, if the original array holds references to objects or arrays, care must be taken not to maintain deep shared references.

Array.reduceRight

Typically we want to reduce from left-to-right. In the case of an array, that means from index 0 to the last index. In case you need to reduce from right-to-left, simply use reduceRight, although the only use case I have come across is to build a custom pipe utility (where you are reducing right-to-left over an array of functions), and that you only do once per project.