Grouping an array of objects by key

Posted 25. July 2019 by Alexander von Studnitz – 6 min read

With the help of array.reduce, the Map data structure and some higher-order functions you can do a lot in JavaScript, for example, building a generic groupBy function for an array of objects.


In this blog post, we are going to implement a generic groupBy function, which when given a collection and a key, returns a new Map() grouped by that key.

For example, given this input:

const blogPosts = [
  { category: 'javascript', author: 'Alex', title: 'Usecases for reduce' },
  { category: 'php', author: 'Alex', title: 'Getting started with phpunit' },
  { category: 'javascript', author: 'Ben', title: 'Maps and Sets' },
]

const blogPostsByCategory = ???

blogPostsByCategory.forEach(category => console.table(category))

The last line should produce the following output (if you didn't already know, console.table() is a hidden gem):

┌─────────┬──────────────┬────────┬───────────────────────┐
│ (index) │   category   │ author │         title         │
├─────────┼──────────────┼────────┼───────────────────────┤
│    0    │ 'javascript' │ 'Alex' │ 'Usecases for reduce' │
│    1    │ 'javascript' │ 'Ben'  │    'Maps and Sets'    │
└─────────┴──────────────┴────────┴───────────────────────┘
┌─────────┬──────────┬────────┬────────────────────────────────┐
│ (index) │ category │ author │             title              │
├─────────┼──────────┼────────┼────────────────────────────────┤
│    0    │  'php'   │ 'Alex' │ 'Getting started with phpunit' │
└─────────┴──────────┴────────┴────────────────────────────────┘

To explain what blogPostsByCategory is doing, we start by introducing the reduce() method. Next, we take a quick look at the concept of higher-order functions and explain the Map data structure. Finally, we put everything together for our generic groupBy function!

reduce()

The reduce() method takes an array and combines its entries into a single value. It does that by executing a reducer function which accumulates the result.

The most common example is computing the sum over all numbers in an array, as shown in the following example:

const numbers = [1, 2, 3]

const add = (a, b) => a + b

const sumOfNumbers = numbers.reduce(add)

console.log(sumOfNumbers)
6

Higher-Order Functions

Higher Order Functions are functions that return a function or take a function as an argument. map(), reduce() and filter() are some notable examples of common higher-order functions.

Currying

Curried functions are functions that return another function which takes exactly one argument.

const addCurried = a => b => a + b

//    add2 is a function, which takes exactly one argument now
const add2 = addCurried(2)
//    addCurried = a => b => a + b
//    add2 =       2 => b => 2 + b
//    add2 =            b => 2 + b

const sumOfnumbersPlus2 = numbers.map(add2).reduce(add)

console.log(sumOfnumbersPlus2)
12

Map<Key,Value>

A Map is a data structure which associated a given key with a value. The key can be anything, a String, a Number or even another Object. The same is true for the value: There are no restrictions as to what you put in a Map.

When iterating over a map, the order in which each entry is visited is the insertion order of the values.

A Map of names starting with a certain character

const namesStartingWithA = ['Alex', 'Adrian']
const namesStartingWithB = ['Ben']
const namesStartingWithC = ['Charlie', 'Carl']

const nameMap = new Map([
  ['A', namesStartingWithA],
  ['B', namesStartingWithB],
  ['C', namesStartingWithC],
])

console.table(nameMap)
┌───────────────────┬─────┬───────────────────────┐
│ (iteration index) │ Key │        Values         │
├───────────────────┼─────┼───────────────────────┤
│         0         │ 'A' │ [ 'Alex', 'Adrian' ]  │
│         1         │ 'B' │       [ 'Ben' ]       │
│         2         │ 'C' │ [ 'Charlie', 'Carl' ] │
└───────────────────┴─────┴───────────────────────┘

Retrieving only the names starting with "A"

We use map.get(key) to do that.

console.log(nameMap.get('A'))
[ 'Alex', 'Adrian' ]

Adding a new name

We use map.set(newValue) to set a new value for that key.

We have to create a new array, where our new name is added to the old names. We set this new array as our value for the key.

nameMap.set('A', [...namesStartingWithA, 'Alf'])
console.log(nameMap.get('A'))
[ 'Alex', 'Adrian', 'Alf' ]

Creating a helper function to append values to an array

Retrieving the array via map.get() can become tedious after a while. Luckily, we can create a higher-order function that helps us to append a value to an existing array for that key, or if it doesn't exist, creates a new one.

We concatenate the old array with the new value using the spread syntax.

// Higher order function, fn(map) -> fn(key) -> fn(value)
const appendValueToArray = map => key => value => {
  // if the map has that key, use that value as array, if not create a new one
  const currentValues = map.get(key) || []

  return map.set(key, [...currentValues, value])
}

appendValueToArray(nameMap)('A')('Albert')
console.log(nameMap.get('A'))
[ 'Alex', 'Adrian', 'Alf', 'Albert' ]

Grouping blogposts by their category

We now have all the tools needed to create a reduce function, which can group our blog posts by their category. Let's get started by "creating" some blog posts.

const blogPosts = [
  { category: 'javascript', author: 'Alex', title: 'Usecases for reduce' },
  { category: 'php', author: 'Alex', title: 'Getting started with phpunit' },
  { category: 'javascript', author: 'Ben', title: 'Maps and Sets' },
]

First, we create our reducer function, making use of the appendValueToArray() method. Next, we retrieve the category of each blog post by performing a destructuring assignment.

// returns the map with our new value appended for the array of the category key
const groupByCategory = (blogPostCategoryMap, blogPost) => {
  const { category } = blogPost
  const addBlogPostsToCategory = appendValueToArray(blogPostCategoryMap)

  return addBlogPostsToCategory(category)(blogPost)
}

Now the missing reduce() call to group our blog posts by category is just one line of code. Note that we have to provide our initial value for our reducer of an empty new Map().

const blogPostsByCategory = blogPosts.reduce(groupByCategory, new Map())

We can then iterate over the map using map.forEach(value, key) to output a pretty list of blog posts for each category.

blogPostsByCategory.forEach((posts, category) => {
  console.log(`Category: ${category}`)
  console.table(posts)
})
Category: javascript
┌─────────┬──────────────┬────────┬───────────────────────┐
│ (index) │   category   │ author │         title         │
├─────────┼──────────────┼────────┼───────────────────────┤
│    0    │ 'javascript' │ 'Alex' │ 'Usecases for reduce' │
│    1    │ 'javascript' │ 'Ben'  │    'Maps and Sets'    │
└─────────┴──────────────┴────────┴───────────────────────┘
Category: php
┌─────────┬──────────┬────────┬────────────────────────────────┐
│ (index) │ category │ author │             title              │
├─────────┼──────────┼────────┼────────────────────────────────┤
│    0    │  'php'   │ 'Alex' │ 'Getting started with phpunit' │
└─────────┴──────────┴────────┴────────────────────────────────┘

Implementing a generic groupBy(key)

We can further generalize this by implementing a generic groupBy method, which groups by any given key.

const groupByKey = key => (mapGroupedByKey, entry) => {
  const addEntryToKey = appendValueToArray(mapGroupedByKey)(entry[key])
  return addEntryToKey(entry)
}

const groupedByKey = (array, key) => array.reduce(groupByKey(key), new Map())

const blogPostsByKey = key => groupedByKey(blogPosts, key)

const blogPostsByAuthor = blogPostsByKey('author')

const blogPostsByCategoryNew = blogPostsByKey('category')

blogPostsByAuthor.forEach(posts => console.table(posts))
┌─────────┬──────────────┬────────┬────────────────────────────────┐
│ (index) │   category   │ author │             title              │
├─────────┼──────────────┼────────┼────────────────────────────────┤
│    0    │ 'javascript' │ 'Alex' │     'Usecases for reduce'      │
│    1    │    'php'     │ 'Alex' │ 'Getting started with phpunit' │
└─────────┴──────────────┴────────┴────────────────────────────────┘
┌─────────┬──────────────┬────────┬─────────────────┐
│ (index) │   category   │ author │      title      │
├─────────┼──────────────┼────────┼─────────────────┤
│    0    │ 'javascript' │ 'Ben'  │ 'Maps and Sets' │
└─────────┴──────────────┴────────┴─────────────────┘

Don't worry if you struggle to wrap around your head around these concepts, it takes some time. But they can be incredibly powerful for working with data in a functional and declarative manner.

const amountOfBlogPostsByAuthor = author => blogPostsByAuthor.get(author).length

const alexBlogPostsCount = amountOfBlogPostsByAuthor('Alex')

console.log(alexBlogPostsCount)
2

Conclusion

In this guide we learned how to utilize array.reduce(), Map and higher-order functions to create a highly flexible method for grouping objects in an array by some key.

Do you have improvements or another neat idea for functional programming in JavaScript? Let me know!

You can find me on Twitter as @jvstudnitz or on my personal blog.


If you liked this article, please consider sharing it with your friends and colleagues.
For questions and feedback, feel free to contact us at hello@clickbar.rocks