🧺Object.groupBy
Object.groupBy splits a list into groups. Your callback returns the group name for each item, and you get back an object with one array per group.
It’s a static method on Object, not an array method, so it works with any iterable: arrays, Sets, Map values, generators. It’s Baseline 2024: all current browsers and Node 21+.
Object.groupBy(items, (item, index) => groupKey)
Basic usage
const inventory = [
{ name: 'asparagus', type: 'vegetable', quantity: 5 },
{ name: 'banana', type: 'fruit', quantity: 0 },
{ name: 'goat', type: 'meat', quantity: 23 },
{ name: 'cherry', type: 'fruit', quantity: 5 },
]
const byType = Object.groupBy(inventory, (item) => item.type)
// {
// vegetable: [{ name: 'asparagus', … }],
// fruit: [{ name: 'banana', … }, { name: 'cherry', … }],
// meat: [{ name: 'goat', … }],
// }
Destructure the groups you want:
const { fruit = [], vegetable = [] } = Object.groupBy(inventory, (item) => item.type)
Make up your own groups
The key doesn’t have to be a field. Return anything you can work out from the item.
// by a range
const byStock = Object.groupBy(inventory, ({ quantity }) =>
quantity === 0 ? 'soldOut' : quantity < 10 ? 'low' : 'ok',
)
// by month
const byMonth = Object.groupBy(posts, (post) => post.date.slice(0, 7)) // '2026-09'
// by first letter, for an A–Z index
const byLetter = Object.groupBy(people, (person) => person.name[0].toUpperCase())
Split into two lists
const { done = [], todo = [] } = Object.groupBy(tasks, (task) => (task.completed ? 'done' : 'todo'))
Return names, not true/false. Keys are always strings, so a boolean becomes 'true' and 'false'.
Group, then summarise
Turn the groups into rows with Object.entries:
const orders = [
{ id: 1, category: 'tech', price: 1200 },
{ id: 2, category: 'home', price: 40 },
{ id: 3, category: 'tech', price: 80 },
]
const summary = Object.entries(Object.groupBy(orders, (order) => order.category)).map(
([category, items]) => ({
category,
count: items.length,
revenue: items.reduce((total, order) => total + order.price, 0),
}),
)
// [
// { category: 'tech', count: 2, revenue: 1280 },
// { category: 'home', count: 1, revenue: 40 },
// ]
If you only need the counts, reduce is lighter, because it doesn’t build the arrays:
const counts = orders.reduce((acc, order) => {
acc[order.category] = (acc[order.category] ?? 0) + 1
return acc
}, {})
// { tech: 2, home: 1 }
Map.groupBy
The same, but it returns a Map. Use it when the key isn’t a string: an object, a number you want to keep as a number, or a date.
const alice = { name: 'Alice' }
const bob = { name: 'Bob' }
const tasks = [
{ title: 'Write tests', owner: alice },
{ title: 'Fix bug', owner: bob },
{ title: 'Deploy', owner: alice },
]
const byOwner = Map.groupBy(tasks, (task) => task.owner)
byOwner.get(alice) // [{ title: 'Write tests', … }, { title: 'Deploy', … }]
A Map also keeps the keys in the order they were first seen, and you can loop over it directly:
for (const [owner, ownerTasks] of byOwner) {
console.log(owner.name, ownerTasks.length)
}
TypeScript
Needs "lib": ["ES2024"] (or ESNext) in tsconfig.json, and TypeScript 5.4+.
type Item = { name: string; type: 'fruit' | 'vegetable' | 'meat' }
const byType = Object.groupBy(inventory, (item) => item.type)
// Partial<Record<'fruit' | 'vegetable' | 'meat', Item[]>>
byType.fruit // Item[] | undefined
The result is Partial, because a group only exists if at least one item returned that key. Handle undefined:
const fruit = byType.fruit ?? []
In React
Group once, then render a heading per group:
function Contacts({ people }: { people: Person[] }) {
const byLetter = Object.groupBy(people, (person) => person.name[0].toUpperCase())
return Object.entries(byLetter).map(([letter, group = []]) => (
<section key={letter} aria-labelledby={`letter-${letter}`}>
<h2 id={`letter-${letter}`}>{letter}</h2>
<ul>
{group.map((person) => (
<li key={person.id}>{person.name}</li>
))}
</ul>
</section>
))
}
Sort the list before you group it. Each group keeps the items in their original order.
Gotchas
- Number-like keys are sorted. Objects put keys such as
'2024'or'3'first, in number order, whatever order they were seen in. UseMap.groupByif the order matters. - The result has no prototype. It’s made with
Object.create(null), soresult.hasOwnProperty(…)andresult.toString()don’t exist. UseObject.hasOwn(result, key)orkey in result.JSON.stringifyandObject.entrieswork as normal. - The groups hold the same objects, not copies. Changing an item in a group changes it in the original list.
- Keys become strings.
undefined,null, numbers and booleans are all turned into strings. Return'none'or similar for items without a value.
Older runtimes
The reduce version, if you can’t use Object.groupBy:
const byType = inventory.reduce((groups, item) => {
;(groups[item.type] ??= []).push(item)
return groups
}, {})
More in Array methods and JS data transformation.