Array.prototype.map
and Array.prototype.reduce
are some of the most powerful array methods in JavaScript. Both serve similar purposes: Create a new value from an array of values. But, what makes them awesome are also what make them different from each other.
Map
The map
method is purpose-built for creating a new array from an array. For example:
const arr1 = [1, 2, 3, 4];
const arr2 = arr1.map((x) => x * 2);
console.log(arr2); // [2, 4, 6, 8]
This is fine and dandy, but in the real world we’re probably working with arrays of objects. Let’s say we have an array of friend objects that each have a name (string) and age (number). We want to just get an array of ages back. Map to the rescue!
const friends = [
{ name: "Sean", age: 23 },
{ name: "Bob the Builder", age: 23 },
];
const ages = friends.map((friend) => friend.age); // [23, 23]
We can even use it with React.js!
const FriendList = () => (
<ul>
{friends.map((friend, i) => (
<li key={i}>
{friend.name}, {friend.age}
</li>
))}{" "}
{/* Equivalent to... */}
{[<li key={0}>Sean, 23</li>, <li key={1}>Bob the Builder, 23</li>]}
</ul>
);
Map is a really neat way with taking an array of items and returning a new array of items. If you want to take an array of items and return any kind of value (array, number, object, etc), you'll want to use another array method...
Reduce
The reduce
array method is for creating a new thing from an array. If you have an array of objects and it should return a single number, Array.prototype.reduce
is a tool for the job. Let’s use the ages
array we mentioned in the Map example above:
const sumAges = ages.reduce(
(accumulator, currentValue) => accumulator + currentValue,
0
); // 46
Let’s break this down.
The reduce
method takes two arguments: A callback function to perform actions on the array and return a value, and an initial value (very important). Here’s the previous code refactored to be more obvious what piece is what:
const callback = (accumulator, currentValue, _indexOfCurrent, _array) =>
accumulator + currentValue;
const initialValue = 0;
const sumAges = ages.reduce(callback, initialValue);
You can see the callback function being passed a few values. It is important to understand that the reduce
method is meant to “squish” all the values in an array into a single value. Yes, you can mimic the exact functionality of map
with reduce
, but that is sort of pointless, no? Why use reduce at all? Reduce accumulates a single value by iterating over an array, performing some sort of computation, and returning a new value to accumulate onto. Rinse, repeat.
accumulator
: This is an important piece of thereduce
puzzle. This represents a possible final value that you want returned from thereduce
method. In thesumAges
function above, we want a number returned. The accumulator will thus be a number. It could be 0, -100, or 1, but a number nonetheless.currentValue
: While iterating over the array, you have access to a single value from the array (the value in which is currently in the iteration). You would most likely use this current value to create a new accumulated value.currentIndex
: This is a non-negative number that represents the index ofcurrentValue
in the array you’re working with.array
: This is the array you are working with. I’ve hardly used it before, but I it can be handy for writing pure functions. I’m guessing this is the array you are working with as it was in memory when you first calledreduce
on it. I haven’t tested that theory, though.
I can hear you asking now: “Well Sean, what is the first value for accumulator
?” I’m glad you asked, observant person. Before we mentioned the initialValue
argument, and that does exactly this. Let’s look again at the sumAges
code from above:
const callback = (accumulator, currentValue, _indexOfCurrent, _array) =>
accumulator + currentValue;
const initialValue = 0;
const sumAges = ages.reduce(callback, initialValue);
You notice that we made initial value 0
. If we didn’t do that, initialValue
would be undefined
. So when we look at the callback function, we can see a problem:
(accumulator, currentValue, _indexOfCurrent, _array) => {
// accumulator = undefined at this point!
return accumulator + currentValue; // undefined + 23 = NaN
};
// sumAges = NaN now!
Real-World
Let's say we're working with an array of objects that looks like this type definition:
type PriceObject {
subTotal: number,
tax: number,
discount?: number
}
You'll notice that discount
is an optional parameter in this object, so it could possibly be undefined. Reduce will make this work perfectly.
Let's work with an array of these PriceObject
s to simulate a shopping cart of sorts, and each item could possibly have a discount
parameter so we can show the user how much they're saving.
Here's what we want at the end of the day:
type ShoppingCart = PriceObject[];
const shoppingCart: ShoppingCart = [
{
subTotal: 15.1,
tax: 0.91,
},
{
subTotal: 12.15,
tax: 0.73,
discount: 3,
},
];
// Right here is where we want to implement reduce
// to combine (or *reduce*) the array into a single thing
const shoppingCartCombined = {
subTotal: 27.25,
tax: 1.64,
discount: 3,
};
Now that we know what we want (a combined shopping cart object combining the parameters), we can solve it with reduce!
const initialPriceObject: PriceObject = {
subTotal: 0,
tax: 0,
discount: 0,
};
const shoppingCartCombined = shoppingCart.reduce(
(acc, curr): PriceObject => ({
subTotal: acc.subTotal + curr.subTotal,
tax: acc.tax + curr.tax,
discount:
curr.discount === undefined ? acc.discount : acc.discount + curr.discount,
}),
initialPriceObject
);
Let's look at this. We intialize our reduce
with an initialPriceObject
which ensures that we have all of our properties defined with an initial value of 0
.
This is important later because it makes sure acc
is an object with all our properties defined with a real value and not undefined
.
In our reduce
callback, we take the accumulated value and current value, and return an object that will always return an object with all properties of PriceObject
defined with at least a 0
. If we wanted to display this on a UI, we can just check if shoppingCartCombined.discount !== 0
and that will always work because it will never be anything other than a number. We have this guaruntee because discount: curr.discount === undefined ? acc.discount : acc.discount + curr.discount
accounts for an object where discount
is not a defined property and just returns the accumulated value if it isn't, and a new value if it is.
Conclusion
map
and reduce
are super powerful. As a developer, I'm happy that it operates in a functional style and is super predictable and testable because of that. They are incredibly useful tools that should be a part of every JavaScript developers toolbelt.