Functional Programming with JavaScript Object Arrays

We look at using map, filter, and reduce to manipulate arrays of objects, using techniques borrowed from functional programming.

Data manipulation is a common task in any JavaScript application. Fortunately, new array handling operators map, filter, and reduce are widely supported. While the documentation for these features is sufficient, it often shows very basic use cases for implementation. In daily use, we often need to use these methods to deal with arrays of data objects, which is the scenario lacking from documentation. In addition, these operators are often seen in functional languages and bring to JavaScript a new perspective on iterating though objects with a functional touch.

In this article, we'll break down a few examples of using map, reduce, and filter to manipulate arrays of objects. Through these examples we'll learn how powerful the methods are, while also understanding how they relate to functional programming.

Functional Programming: The Good Parts

Functional programming has many concepts that reach beyond the scope of what we're trying to accomplish. For the context of this article we'll discuss some of the absolute basics.

Throughout the examples we will favor single expressions over multiple statements. This means we'll greatly reduce the number of variables used and instead utilize function composition and method chaining whenever possible. This style of programming reduces the need to maintain state, which aligns with functional programming practices.

One common benefit of using map and filter is that each operator creates a new array. By creating new arrays instead of modifying or mutating existing data, our code will be easier to reason about, since mutating data can cause unintended side-effects.

Finally, we'll take advantage of the fact that each operator is a higher order function. Simply put, each operator accepts a callback function as a parameter. The callback function allows us to customize behavior through function delegation, a powerful tool for code flexibility and readability.

The idea here isn't to go "fully functional" but to leverage concepts when convenient. If these concepts connect with you, a cheat sheet with a list of JavaScript methods/functions frequently used in functional programming is available for download. The Functional Programming Cheat Sheet is a great quick reference to keep handy.

Download the Cheat Sheet Now

Arrows Everywhere / Function Expressions

When coming form older JavaScript libraries the "fat arrow" might seem a little foreign. This => arrow operator is used to reduce the amount of code need to write a function. Instead of explicitly writing the function() with a return statement when we need a simple function we can use Identifier => Expression. This helps make our code easier to read, especially when using higher order functions, methods that take a function as a parameter. .map, .reduce, and .filter are all higher order functions.

Without function expressions, we might write something like this:

let cart = [
  { name: "Drink", price: 3.12 },
  { name: "Steak", price: 45.15},
  { name: "Drink", price: 11.01}
];

let steakOrders = cart.filter(function(obj) {
  return obj.name === "Steak"
});

// { name: "Steak", price: 45.15 }

With the arrow operator, we might consider writing the same code omitting the function like the example below:

let steakOrders = cart.filter((obj) => { return obj.name === "Steak" });

// { name: "Steak", price: 45.15 }

However, we can take this a step further because most of the expression is already implied when using an arrow operator. The code can further be reduced:

let steakOrders = cart.filter(obj => obj.name === "Steak");

// { name: "Steak", price: 45.15 }

In this example, we can see that the arrow helped define a concise function within the filter statement. Now that we've seen how the arrow can improve how expressions are written, let's learn more about using the filter method.

Filtering Object Arrays

The filter method creates a new array with all elements that pass the test implemented by the provided function. When using the filter method against objects, we can create a restricted set based on one or more properties on the object.

In this example we'll get the objects in the collection with the name "Steak" by passing in a function expression that tests the name property's value, obj => obj.name === "Steak":

let cart = [
  { name: "Drink", price: 3.12 },
  { name: "Steak", price: 45.15},
  { name: "Drink", price: 11.01}
];

let steakOrders = cart.filter(obj => obj.name === "Steak");

// [{ name: "Steak", price: 45.15 }]

Consider that we my need a subset of data where we need to filter on multiple property values. We can accomplish this by writing an function that contains multiple tests:

let expensiveDrinkOrders =
    cart.filter(x => x.name === "Drink" && x.price > 10);

// [{ name: "Drink", price: 11.01 }]

When filtering on multiple values, the function expression can get lengthy which makes the code tough to reason upon at a glance. Using a few techniques, we can simplify the code and make it more manageable.

One way to approach the problem is by using multiple filters in place of the && operator. By chaining together multiple filters we can make the statement easier to reason about while producing the same results:

let expensiveDrinkOrders = cart.filter(x => x.name === "Drink")
                               .filter(x => x.price > 10);

// [{ name: "Drink", price: 11.01 }]

Another way we can express a complex filter is by creating a named function. This will allow us to write the filter in a way that is "human readable":

const drinksGreaterThan10 =
  obj => obj.name === "Drink" && obj.price > 10;
let result = cart.filter(drinksGreaterThan10);

While this does work as intended the price value is hardcoded. We can optimize for readability and flexibility by allowing the price to be set at run-time using a parameter.

At first it might seem intuitive to add the parameter with obj such that it reads (obj, cost), where cost is the variable price:

const drinksGreaterThan =
  (obj, cost) => obj.name === "Drink" && obj.price > cost;
let result = cart.filter(drinksGreaterThan(10)); // Error

Currying in JavaScript

Unfortunately, this would create an error because filter expects a function and now we're trying to pass the output of drinksGreaterThan. To satisfy the parameters correctly, we would need to write the filter statement as .filter(x => drinksGreaterThan(x, 10)).

To provide a better solution, we can use a functional programming technique called currying. Currying allows a function with multiple arguments to be translated into a sequence of functions, thus allowing us to create parity with other function signatures.

Let's move the cost parameter to a function expression and rewrite the drinksGreaterThan function using currying. This is just clever use of a higher order function, where drinksGreatThan becomes a function that accepts a cost and returns another function which accepts an obj:

const drinksGreaterThan =
  cost => obj => obj.name === "Drink" && obj.price > cost;
let result = cart.filter(drinksGreaterThan(10));

// [{ name: "Drink", price: 11.01 }]

The completed function gives us a named filter with maximum readability and re-usability.

Mapping Objects

The map method is an essential tool for converting and projecting data. The map method creates a new array with the results of calling a provided function on every element in the calling array.

In a scenario where we consume an external data source, we my be provided with more data than is needed. Instead of dealing with the complete data set, we can use map to project the data as we see fit. In the following example, we'll receive data that includes several properties, most of which aren't used in our current view.

The initial object contains: id, name, price, cost, and size:

let jsonData = [
  { id: 1, name: "Soda", price: 3.12, cost: 1.04, size: "4oz", },
  { id: 2, name: "Beer", price: 6.50, cost: 2.45, size: "8oz" },
  { id: 3, name: "Margarita", price: 12.99, cost: 4.45, size: "12oz" }
];

For our view, we'll only be using the name and price properties, so we'll use map to construct a new data set:

let drinkMenu = jsonData.map(x => ({name: x.name, price: x.price}));

// [{"name":"Soda","price":3.12}, {"name":"Beer","price":6.5}, {"name":"Margarita","price":12.99}]

Using map, we can assign values from each item to a new object which only has the name and price properties.

If we're concerned about readability of the map function expression we can assign the function to a variable. This abstraction doesn't change the way map operates, however the intent of the code becomes clearer to those who may not be familiar with the domain.

By creating a unique method named toMenuItem we can immediately give context to our code. The map statement becomes self-documenting as it can be read aloud "JSON data, map to menu item".

const toMenuItem = x => ({name: x.name, price: x.price});
let drinkMenu = jsonData.map(toMenuItem);

// [{"name":"Soda","price":3.12}, {"name":"Beer","price":6.5}, {"name":"Margarita","price":12.99}]

Continuing with this example we'll use map again to apply a value conversion. Let's assume that we want to convert the existing menu prices from USD to Euro. Since map does not mutate the initial array, we don't need to worry about the state of the drinkMenu instance changing because of our conversion.

Let's use a similar function to convert the price values, except this time we'll keep all of the properties the object has available. To ensure we copy every value we'll use the ... spread operator:

const drinkMenuEuro = drinkMenu.map(x => ({...x, price: (x.price * 0.81).toFixed(2)})

// [{"name":"Soda","price":"2.53"},{"name":"Beer","price":"5.27"},{"name":"Margarita","price":"10.52"}]

In this example, ...x (the spread operator) does a lot for us. This small bit of code ensures that we copy the complete object and its properties while only modifying the price value. The benefit of using the spread operator is that we can add additional properties later to drinkMenu and they will be included drinkMenuEuro automatically.

Reduce with Objects

Reduce is an underused and sometimes misunderstood array operator. The reduce method applies a function against an accumulator and each element in the array (from left to right) to reduce it to a single value.

When using reduce with an array of objects the accumulator value represents the object resulting from the previous iteration. If we need to get the total value of a objects property (i.e. obj.price), we first need to map that property to a single value:

let cart = [
  { name: "Soda", price: 3.12 },
  { name: "Margarita", price: 12.99},
  { name: "Beer", price: 6.50}
];

let totalPrice = cart.reduce((acc,next) => acc.price + next.price);
// NaN because acc cannot be both an object and number

let totalPrice = cart.map(obj => obj.price).reduce((acc, next) => acc + next);
// 22.61

let totalPrice = cart.reduce((acc,next) => acc.price + next.price, 0);
// Setting the initial value to 0 will initialize the accumulator as a number.
// 22.61

In the previous example, we saw that when we reduce an array of objects then the accumulator is object. The reduce method is extremely useful on objects under the right conditions. Instead of attempting to sum the price of an object, let's use reduce to find the object with the largest price:

let mostExpensiveItem = cart.reduce((acc, next) => acc.price > next.price ? acc : next);

// { name: "Margarita", price: 12.99}

Here, we leverage the functionality of reduce to give us the largest object.

We can use a function delegate to describe our intent. This will help give clarity to the expression inside of the reduce operator. Our final function reads "reduce by greatest price":

let byGreatestPrice = (item, next) => item.price > next.price ? item : next;
let mostExpensiveItem = cart.reduce(byGreatestPrice);

// { name: "Margarita", price: 12.99}

Conclusion

In this article we looked at using map, reduce, and filter to manipulate arrays of objects. Since these array operators don't modify the state of the calling array, we can use them effectively without the worry of side-effects. Using techniques borrowed from functional programming we can write powerful expressive array operators. Through function delegation we have the option of making explicit function names to create readable code.

If the examples showing map, reduce, and filter have piqued your interest in functional programming, then check out the Functional Programming Cheat Sheet for more examples of using a functional approach to building JavaScript applications.

Check out the Cheat Sheet

Related content:

Thank you readers
I'd like to thank the readers who contributed with their comments for helping improve the quality of this article: 0hlala, Emilio Lopez, and Karsten.
Ed Charbeneau is a Developer Advocate for Telerik
About the Author

Ed Charbeneau

Ed Charbeneau is a web enthusiast, speaker, writer, design admirer, and Developer Advocate for Telerik. He has designed and developed web based applications for business, manufacturing, systems integration as well as customer facing websites. Ed enjoys geeking out to cool new tech, brainstorming about future technology, and admiring great design. Ed's latest projects can be found on GitHub.

Related Posts

Comments

Comments are disabled in preview mode.