Here's a quick intro to the reduce()
method in Javascript/Typescript arrays, which is frequently perplexing when encountered in working code.
The code here is written in Typescript but I've tried to keep it friendly for JS readers, and I'll post a link to the equivalent JS at the end.
What's the point of reduce?
Reduce
allows us to take a container of data (like an array) and fold it into another data structure.
The reduce()
method involves three parts:
- A container of values, like an array, with which we need to update another structure (in sequence)
- A function that lets us update a value (typically called the accumulator) based on an element from our array
function updater(accumulator:SomeType, nextValueFromArray): SomeType {
... // whatever operations we want
return updatedAccumulator
}
Frequently this updater is written inline, directly inside the reduce function.
- The last thing the reducer needs is an initial value for our accumulator, for the first iteration of the function.
Reduce
is smart enough to realize that if we don't provide an initial value, it should use the first element of the array as the initial value.
NOTE: Omitting the initial value only works if the accumulator is the same type as elements. An example to show this will be provided below.
So, to recap, any reduce operation can be thought of as
someArrayOfValues.reduce(updater, initialValueOfTheAccumulator)
Examples
Let's look at some examples!
First, let's see how we could do string concatenation using reduce. This involves 'folding' an array of strings into a single string.
// our array of characters to fold
const boSpelling = ['B', 'o', ' ', 'B', 'u', 'r', 'n', 'h', 'a', 'm']
// our initial value for us to reduce into is an empty string
const initialName = ''
Here you see, we write a function that understands how to add a letter to a value and return a new value. Reduce
takes that function, and our new value, and will pass each letter of our array to that function, bringing the result forward to serve as the accumulated value for the next iteration.
const bosName = boSpelling.reduce((nameSoFar, letter) => {
const updatedName = nameSoFar + letter
return updatedName
}, initialName)
We could also inline the initial value.
const bosName = boSpelling.reduce((nameSoFar, letter) => {
const updatedName = nameSoFar + letter
return updatedName
}, '')
console.log(bosName) // "Bo Burnham"
Just to provide context, here is the for
loop version. This does the same as the code above, but here we update a mutable variable and use a for
block instead of a function expression.
Some people find this preferable but it does require object mutation, unlike reduce
.
const concatenate = (lst:string[]) => {
let name = ""
for (let letter of lst) {
name += letter
}
return name
}
const bosName = concatenate(boSpelling)
console.log(bosName) \\ "Bo Burnham"
Now, let's make a custom sum function using reduce
. Combining with es6 syntax allows for some very concise expressions.
const numbers = [ 2, 3, 4, 5, 6, 7, 8, 9, 10]
const sum = (lst:number[]) =>
lst.reduce((count, nextNum) => count + nextNum, 0)
Note that since the accumulator count
, and the array elements are both numbers, we can omit the initial value and just let reduce
use the first value as the initial.
In situations where they're not the same data type, doing this would cause an error.
const sum = (lst:number[]) =>
lst.reduce((count, nextNum) => count + nextNum)
console.log(sum(numbers)) // "54"
Advanced Examples
We've hit the end of the main things I wanted to demonstrate with reduce (I told you this would be quick). But we can have a bit more fun and show how powerful and flexible it really is. These next examples are beyond the standard use cases of reduce and if you're still new to the concept, feel free to skip them.
The reduce()
method can fold a sequence of values into any data structure you'd like, including another sequences.
This makes it more powerful than its sibling methods, map()
and filter()
, which can only transform an array into another array. But that doesn't mean it can't do what they do as well.
Here we make map()
from reduce. For each item in the original array, we apply the function to it and add to an accumulator, a new array.
const map = <a, b>(func:(arg:a) => b, lst:a[]) =>
lst.reduce((acc:b[], item) => [...acc, func(item)], [])
and we can use it similarly to the map()
method we know
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
const multiplyByThree = (x:number) => x * 3
const mapResults = map(multiplyByThree, numbers)
console.log(mapResults) \\ "3,6,9,12,15,18,21,24,27,30"
The filter
function is similar. Here, the function we pass in is a condition, which is a function that accepts a variable of the same type as the ones in the array and returns a boolean).
If the array item satisfies the condition (returns true), we add it to the new array, otherwise we pass along the new array as is.
const filter = <a>(condition:(arg:a) => boolean, lst:a[]) =>
lst.reduce((newLst:a[], item) =>
condition(item) ? [...newLst, item] : newLst, [])
// our condition
const isEven = (x:number) => x % 2 === 0 ? true : false
const filterResults = filter(isEven, numbers)
console.log(filterResults) \\ "2,4,6,8,10"
A brief aside on types
Another way we can compare the three methods in in terms of the types
they accept, and return. In pseudocode, the types of the three functions can be described as
map : (a -> b), Array a -> Array b
Given a function that takes an a
and returns a b
, and an array of a
, map
will return an array of b
.
filter : (a -> Bool) -> Array a -> Array a
Given a function that takes an a
and returns a boolean
, and an array of a
, filter returns an array of a
reduce : (b -> a -> b) -> b -> Array a -> b
Given a function that takes a b
and an a
and returns a b
, an initial accumulator value b
, and an array of a
, reduce
returns a b
.
Final Thoughts
I hope this provided some clarity and demystified one of the more powerful tools in the JS toolbelt.
Let me know if this helped, or what other methods you want five minutes on!
Resources
- See here for the full code in a sandboxed environment, in both TS and JS versions.
- See here for some more official docs on the method, it's overloads, etc.
- Note there's a small error in the live example where the
isOdd
function actually checks for evenness, and I am being too lazy to fix it and get a new URL.
Top comments (1)
Wonderful man 👍