Array grouping is a task you likely have implemented in JavaScript. If you use SQL, it’s similar to doing a GROUP BY
. Given a dataset, we can compose a higher-level dataset by putting like data in a group and identifying each group by a given identifier.
I’ll dive into the new Array grouping functions released this year (2024), namely, Object.groupBy
and Map.groupBy
.
TypeScript support for these APIs is available from TypeScript 5.4 for my TypeScript fans. However, you’d have to configure your tsconfig.json
to target ESNext
. When they’re available in ES2024, you can set the target to ES2024
or higher.
Array Grouping pre-2024
Array grouping in JavaScript is not a new concept, we have implemented it in various. It could be done with a for
or foreach
loop, Array.prototype.reduce
, or groupBy function in underscore.js or lodash.
Given a list of employees, here’s how we would group the data using reduce()
:
interface Employee { name: string; department: string; age: number; manager?: Employee; joined: Date
} const ceo = { name: "John Doe", department: "engineering", age: 47, joined: new Date("10-04-2020")
} const cfo = { name: "Anna Maria", department: "finance", age: 45, joined: new Date("10-05-2020") } const employees: Employee[] = [ ceo, { name: "Pop Jones Jr.", department: "finance", age: 30, manager: cfo, joined: new Date("10-04-2021") }, { name: "Sarah Clara", department: "engineering", age: 32, manager: ceo, joined: new Date("10-05-2021") }, cfo, { name: "Funmi Kola", department: "engineering", age: 20, manager: ceo, joined: new Date("10-05-2022") }, { name: "Julius Maria", department: "sales", age: 27, manager: cfo, joined: new Date("10-05-2022") } ] const groupByDepartment = employees.reduce<Record<string, Employee[]>>((acc, employee) => { const department = employee.department; if (acc[department] === undefined) { acc[department] = []; } acc[department].push(employee); return acc;
}, {}); console.log(groupByDepartment);
The output for groupByDepartment
should be:
{ "engineering": [ { "name": "John Doe", "department": "engineering", "age": 47, "joined": "2020-10-03T22:00:00.000Z" }, { "name": "Sarah Clara", "department": "engineering", "age": 32, "manager": { "name": "John Doe", "department": "engineering", "age": 47, "joined": "2020-10-03T22:00:00.000Z" }, "joined": "2021-10-04T22:00:00.000Z" }, { "name": "Funmi Kola", "department": "engineering", "age": 20, "manager": { "name": "John Doe", "department": "engineering", "age": 47, "joined": "2020-10-03T22:00:00.000Z" }, "joined": "2022-10-04T22:00:00.000Z" } ], "finance": [ { "name": "Pop Jones Jr.", "department": "finance", "age": 30, "manager": { "name": "Anna Maria", "department": "finance", "age": 45, "joined": "2020-10-04T22:00:00.000Z" }, "joined": "2021-10-03T22:00:00.000Z" }, { "name": "Anna Maria", "department": "finance", "age": 45, "joined": "2020-10-04T22:00:00.000Z" } ], "sales": [ { "name": "Julius Maria", "department": "sales", "age": 27, "manager": { "name": "Anna Maria", "department": "finance", "age": 45, "joined": "2020-10-04T22:00:00.000Z" }, "joined": "2022-10-04T22:00:00.000Z" } ]
}
Grouping With Object.groupBy
The Object.groupBy
function is used to group by a given string value. That means: given an iterable, group its elements according to the value returned by the provided callback function. The value returned from the callback should be something that can be coerced into a string or symbol.
Object.groupBy
returns a null-prototype object which has separate properties for each group, containing arrays with the elements in the group. Using null-prototype object allows for ergonomic destructuring and prevents accidental collisions with the global Object properties.
Using the same dataset, we can group the employees by their department using Object.groupBy
:
const groupByDepartment = Object.groupBy(employees, ({department}) => department)
This gives us the same result as the reduce
method but with less code. The output for groupByDepartment
should be:
{ "engineering": [ { "name": "John Doe", "department": "engineering", "age": 47, "joined": "2020-10-03T22:00:00.000Z" }, { "name": "Sarah Clara", "department": "engineering", "age": 32, "manager": { "name": "John Doe", "department": "engineering", "age": 47, "joined": "2020-10-03T22:00:00.000Z" }, "joined": "2021-10-04T22:00:00.000Z" }, { "name": "Funmi Kola", "department": "engineering", "age": 20, "manager": { "name": "John Doe", "department": "engineering", "age": 47, "joined": "2020-10-03T22:00:00.000Z" }, "joined": "2022-10-04T22:00:00.000Z" } ], "finance": [ { "name": "Pop Jones Jr.", "department": "finance", "age": 30, "manager": { "name": "Anna Maria", "department": "finance", "age": 45, "joined": "2020-10-04T22:00:00.000Z" }, "joined": "2021-10-03T22:00:00.000Z" }, { "name": "Anna Maria", "department": "finance", "age": 45, "joined": "2020-10-04T22:00:00.000Z" } ], "sales": [ { "name": "Julius Maria", "department": "sales", "age": 27, "manager": { "name": "Anna Maria", "department": "finance", "age": 45, "joined": "2020-10-04T22:00:00.000Z" }, "joined": "2022-10-04T22:00:00.000Z" } ]
}
Grouping With Map.groupBy
Map.groupBy
is similar to Object.groupBy
, except that it returns a Map and the given iterable can be grouped using any arbitrary value. In this case, the dataset can be grouped using an object.
Looking back to the dataset, we defined a manager
property for some employees and assigned it the ceo
or cfo
object. We can group the employees by their manager
, which would look like this:
const managerWithTeammates = Map.groupBy(employees, ({manager}) => manager)
The output for managerWithTeammates
should look like:
Map (3) {undefined => [{ "name": "John Doe", "department": "engineering", "age": 47, "joined": "2020-10-03T22:00:00.000Z"
}, { "name": "Anna Maria", "department": "finance", "age": 45, "joined": "2020-10-04T22:00:00.000Z"
}], { "name": "Anna Maria", "department": "finance", "age": 45, "joined": "2020-10-04T22:00:00.000Z"
} => [{ "name": "Pop Jones Jr.", "department": "finance", "age": 30, "manager": { "name": "Anna Maria", "department": "finance", "age": 45, "joined": "2020-10-04T22:00:00.000Z" }, "joined": "2021-10-03T22:00:00.000Z"
}, { "name": "Julius Maria", "department": "sales", "age": 27, "manager": { "name": "Anna Maria", "department": "finance", "age": 45, "joined": "2020-10-04T22:00:00.000Z" }, "joined": "2022-10-04T22:00:00.000Z"
}], { "name": "John Doe", "department": "engineering", "age": 47, "joined": "2020-10-03T22:00:00.000Z"
} => [{ "name": "Sarah Clara", "department": "engineering", "age": 32, "manager": { "name": "John Doe", "department": "engineering", "age": 47, "joined": "2020-10-03T22:00:00.000Z" }, "joined": "2021-10-04T22:00:00.000Z"
}, { "name": "Funmi Kola", "department": "engineering", "age": 20, "manager": { "name": "John Doe", "department": "engineering", "age": 47, "joined": "2020-10-03T22:00:00.000Z" }, "joined": "2022-10-04T22:00:00.000Z"
}]}
With this example, we have 3 groups:
The CEO and CFO who doesn’t have a manager, with
undefined
as the Map object key.The CFO, Anna Maria, who has two employees under her. This uses the
cfo
object as the Map object key.The CEO, John Doe, who has two employees under him. This uses the
ceo
object as the Map object key.
Conclusion
Isn’t it amazing the new APIs that are coming to JavaScript? The Object.groupBy
and Map.groupBy
functions make it easier to group data in an array or iterable, and they are more ergonomic than the traditional reduce
method. They also reduce your bundle size by not requiring an external library like lodash
or underscore.js
.
To recap, use the Map.groupBy
function primarily when grouping elements associated with any arbitrary object, particularly when that object might change over time. If the object is invariant, you might instead represent it using a string, and group elements with Object.groupBy()
. If using TypeScript, set the target to ESNext
(or ES2024
when it’s available in the future).
If you have any questions or suggestions, feel free to leave a comment. You can also reach me on Twitter. Thanks for reading 😎
You will find a playground with the code used in this blog post using this link.
If you’re interested in similar topics but in video format, subscribe to my YouTube channel: www.youtube.com/@pmbanugo
Originally authored and published by me on Telerik’s blog
Discover more from Coursity
Subscribe to get the latest posts sent to your email.