Map and Reduce JavaScript Arrays

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 the reduce puzzle. This represents a possible final value that you want returned from the reduce method. In the sumAges 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 of currentValue 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 called reduce 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 PriceObjects 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.