DEV Community

Miki Stanger
Miki Stanger

Posted on

Pure Reduce Functions - Use Aggregator As A State

when using Array.reduce() for complex operations, we tend to resort to using side effects (e.g. defining a let variable or an object out of scope. and modify them while iterating).

Here's an easy way to keep your reduce functions pure by using the aggregator as a state.


Consider a reduce function that's meant to sum all numbers, and add the largest number twice.
We can declare a mutable variable before, use it to track the biggest number, then add it after we're done reducing:

let biggestNum = Number.NEGATIVE_INFINITY;

const sum = arr.reduce(
  (agg, val) => {
    biggestNum = Math.max(val, biggestNum);
    return agg + val;
  },
  0,
);

const sumAndBiggest = sum + biggestNum;
Enter fullscreen mode Exit fullscreen mode

Alternatively, we can encapsulate all related logic INSIDE the reduce function:

const sumAndBiggest = arr.reduce(
  (agg, val, index, sourceArr) => {
    if (index === sourceArr.length - 1) {
      return agg.sum + val + agg.biggestNum;
    }

    return {
      sum: agg.sum + val,
      biggestNum: Math.max(val, agg.biggestNum),
    };
  },
  { sum: 0, biggestNum: Number.NEGATIVE_INFINITY },
);
Enter fullscreen mode Exit fullscreen mode

You'll notice two things:

  • We use the aggregator as a state - we include the actual sum we're reducing, and the biggest number we found so far.
  • We use the 3rd and 4th arguments of the reduce function - the current index and the reference to the array we're iterating over - to see if the current iteration is the last. If it is, we add the largest number and return the final sum instead of a new state.

This way adds a bit more logic to the reduce function. Why should we use it then?
Because it allows us to encapsulate all of the data needed for the reduction operation. This means:

  • You don't get leftover variables after the reduction is done. When someone reads the code or debugs it, they won't have to worry about those variables - biggestNum in our first example - are being used elsewhere.
  • Your reduce function is a pure function. Everything you need to look at happens inside the function and its arguments. This also means easier debugging.
  • All of the state's data is garbage-collected when it's no longer used.

Unfortunately, TypeScript only offers an option to define one return value for reduce, and doesn't allow us to differentiate between the final and intermediary return values.
This means we'll have to define the reduce function as being able to return both types, and use the as keyword to assert our aggregator's type before use:

interface IReduceState {
  sum: number,
  biggestNum: number;
}
const sumAndBiggest = arr.reduce<number | IReduceState>(
  (agg, val, index, sourceArr) => {
    const { sum, biggestNum } = agg as IReduceState
    if (index === sourceArr.length - 1) {
      return sum + val + biggestNum;
    }

    return {
      sum: sum + val,
      biggestNum: Math.max(val, biggestNum),
    };
  },
  { sum: 0, biggestNum: Number.NEGATIVE_INFINITY },
);
Enter fullscreen mode Exit fullscreen mode

If you have a nicer solution to this, please let me know!

Happy coding!


Thank you, Yonatan Kra, for your kind review.

Discussion (0)