Agrupando Arrays em JavaScript com Object.groupBy
Object.groupBy em JavaScript agrupa arrays por chave, compara reduce e Map.groupBy, e explica coerção para string e resultados sem protótipo.
Object.groupBy(items, callback) agrupa um array em uma única chamada: executa o callback uma vez para cada elemento, usa o valor retornado como nome do grupo e devolve um objeto contendo um array de elementos correspondentes sob cada nome.
Muitas bases de código ainda carregam um reduce escrito à mão para isso, com as mesmas poucas linhas de boilerplate de acumulador copiadas entre arquivos, ou um import do Lodash mantido vivo por causa de uma única chamada a groupBy. Nenhuma das duas abordagens está errada, e ambas continuam funcionando; com o método nativo disponível, porém, nenhuma delas é necessária.
Este artigo mostra a chamada nativa aplicada a um problema concreto (pedidos por status), coloca-a ao lado do reduce que ela substitui, explica quando Map.groupBy é a ferramenta certa e percorre os dois comportamentos que costumam derrubar as pessoas em produção: chaves que silenciosamente viram strings e um objeto de resultado que não possui hasOwnProperty.
Principais Conclusões
Object.groupBychama seu callback com dois argumentos,(element, index), e usa o valor de retorno como chave do grupo.- Substituir um acumulador
reduceporObject.groupByé uma mudança de legibilidade, não de performance; não há motivo para esperar que ele execute mais rápido. - Use
Map.groupByquando a chave de agrupamento não for uma string: um objeto, umDateou um número que você precisa manter como número. - Agrupar por um booleano ou número com
Object.groupByproduz as chaves string"true"e"40", porque toda chave é coagida para uma property key. - O objeto retornado por
Object.groupBytem prototype nulo, entãoresult.hasOwnProperty(...)lança umTypeError; useObject.hasOwnou copie o resultado com spread.
Agrupando Pedidos por Status em Três Linhas
Dado um array de objetos de pedido, Object.groupBy produz um objeto indexado por status em uma única expressão, sem acumulador e sem verificação de existência.
const orders = [
{ id: 1, status: "shipped", total: 40 },
{ id: 2, status: "pending", total: 15 },
{ id: 3, status: "shipped", total: 60 },
{ id: 4, status: "refunded", total: 22 },
];
const byStatus = Object.groupBy(orders, (order) => order.status);
console.log(Object.keys(byStatus)); // ["shipped", "pending", "refunded"]
console.log(byStatus.shipped.length); // 2
A referência de Object.groupBy() no MDN descreve o contrato: o primeiro argumento é qualquer iterável, não apenas um array, e o resultado carrega uma propriedade por chave distinta. Os grupos aparecem na ordem em que seu primeiro membro foi encontrado. Os elementos dentro de cada grupo são os objetos originais, não cópias, então mutar byStatus.shipped[0] também muta orders[0].
Como Funciona o Callback do Object.groupBy?
O callback recebe dois argumentos, o elemento atual e seu índice, e o que quer que ele retorne se torna a chave do grupo daquele elemento. A definição de Object.groupBy no ECMA-262 especifica exatamente esses dois argumentos; não há um terceiro argumento com “o array inteiro”, como acontece em map ou filter.
Como a chave é computada, o callback não se limita a ler um campo. Qualquer expressão que produza uma string funciona, incluindo uma comparação ou um bucket derivado do índice:
const bySize = Object.groupBy(orders, (order) => (order.total >= 50 ? "large" : "small"));
// { small: [order 1, order 2, order 4], large: [order 3] }
const byHalf = Object.groupBy(orders, (_, index) => (index < 2 ? "first" : "second"));
// { first: [order 1, order 2], second: [order 3, order 4] }
Todo elemento cai em exatamente um grupo. Se dois elementos produzirem a mesma chave, eles compartilham um array na ordem de inserção.
Group By em JavaScript com reduce vs Object.groupBy
Substituir um acumulador reduce por Object.groupBy elimina o objeto inicial, a verificação de existência por elemento, a alocação do array, o push e o return; o que resta é a única linha que decide a qual grupo um elemento pertence.
Aqui está a versão que a maioria das bases de código já contém, escrita da forma mais compacta que a atribuição lógica de nullish permite:
const byStatusReduce = orders.reduce((acc, order) => {
acc[order.status] ??= [];
acc[order.status].push(order);
return acc;
}, {});
E o mesmo resultado com o método nativo:
const byStatus = Object.groupBy(orders, (order) => order.status);
Ambos produzem agrupamentos equivalentes. A diferença está no que o leitor precisa manter na cabeça. Na forma com reduce, a intenção de agrupamento está espalhada por um objeto inicial do acumulador, uma alocação condicional, uma mutação e um valor de retorno, e qualquer um desses quatro pontos pode estar sutilmente errado. Na forma nativa, a única coisa que resta revisar é a função de chave.
Object.groupBy é uma mudança de legibilidade, não de performance. Ambas as abordagens iteram a entrada uma vez e alocam um array por grupo, e não há motivo para esperar que a chamada nativa seja mais rápida que um reduce bem escrito. Escolha-a porque remove boilerplate, não por causa de um benchmark.
Quando Você Deve Usar Map.groupBy?
Use Map.groupBy quando a chave de agrupamento não for uma string: um objeto, um Date ou um número que você precisa manter como número. Map.groupBy() aceita o mesmo callback de dois argumentos e difere apenas no tipo de retorno, um Map cujas chaves são exatamente os valores retornados pelo callback.
const byTotal = Map.groupBy(orders, (order) => order.total);
console.log([...byTotal.keys()]); // [40, 15, 60, 22]
console.log(typeof [...byTotal.keys()][0]); // "number"
console.log(byTotal.get(40).length); // 1
As chaves voltam como números, na ordem de inserção, e são lidas com .get(). O próprio exemplo do MDN na página de Map.groupBy agrupa por identidade de objeto, que é o caso que um objeto literal não consegue lidar de jeito nenhum: dois objetos distintos com conteúdos idênticos permanecem distintos como chaves de Map.
Object.groupBy | Map.groupBy | |
|---|---|---|
| Tipo de chave | Coagida para string ou symbol | Qualquer valor, mantido como está |
| Tipo de retorno | Objeto com prototype nulo | Map |
| Ler um grupo | result.shipped | result.get(key) |
| Ordem de iteração das chaves | Ordem de inserção, exceto chaves tipo inteiro, que são ordenadas de forma ascendente | Ordem de inserção |
Por Que Object.groupBy Transforma Chaves Numéricas em Strings?
Agrupar por um booleano ou um número com Object.groupBy produz as chaves string "true" e "40", não os valores originais true e 40; Map.groupBy preserva os valores originais como chaves do Map. Tudo o que o callback devolve precisa terminar como uma property key, então qualquer coisa que ainda não seja uma string ou um symbol é convertida em string no caminho. Esse é o comportamento comum de objetos, mas ainda assim pega de surpresa quem espera um resultado indexado por booleano.
const byPaid = Object.groupBy(orders, (order) => order.status === "shipped");
console.log(Object.keys(byPaid)); // ["true", "false"]
console.log(typeof Object.keys(byPaid)[0]); // "string"
console.log(byPaid[true] === byPaid["true"]); // true (lookup coerces too)
const byTotalObj = Object.groupBy(orders, (order) => order.total);
console.log(Object.keys(byTotalObj)); // ["15", "22", "40", "60"]
Duas coisas acontecem no caso numérico. Os totais viraram strings, e voltaram ordenados de forma ascendente em vez de na ordem de inserção, porque property keys do tipo inteiro são enumeradas em ordem numérica ascendente antes das demais chaves string. Código que itera o resultado esperando a sequência original vai renderizar os grupos na ordem errada sem lançar nenhum erro.
Session replays de bugs de agrupamento frequentemente mostram exatamente esse formato: um cabeçalho de categoria que exibe true em vez de “Shipped”, ou buckets aparecendo ordenados quando os dados não estavam. O console está limpo, o formato dos dados parece correto em um log porque { true: [...] } é impresso de forma idêntica quer a chave seja um booleano ou uma string, e só ao ver a UI renderizada ao lado do caminho de código a coerção fica óbvia.
Por Que hasOwnProperty Lança Erro em um Resultado de Object.groupBy?
O objeto retornado por Object.groupBy tem prototype nulo, então result.hasOwnProperty("shipped") lança um TypeError; use Object.hasOwn(result, "shipped") ou copie o resultado com sintaxe de spread se o código downstream esperar um objeto comum. O MDN documenta o valor de retorno como um objeto com prototype nulo, o que significa que nada de Object.prototype é alcançável através dele: sem hasOwnProperty, sem toString, sem valueOf.
const byStatus = Object.groupBy(orders, (o) => o.status);
byStatus.hasOwnProperty("shipped");
// TypeError: byStatus.hasOwnProperty is not a function
Object.hasOwn(byStatus, "shipped"); // true
"shipped" in byStatus; // true
Object.keys(byStatus); // ["shipped", "pending", "refunded"]
JSON.stringify(byStatus); // works normally
const plain = { ...byStatus }; // ordinary object with Object.prototype
plain.hasOwnProperty("shipped"); // true
O MDN aponta Object.hasOwn como o substituto moderno de hasOwnProperty, e ele está em Baseline Widely available desde março de 2022, então você pode usá-lo diretamente. Object.keys, Object.entries, o operador in, JSON.stringify e o spread funcionam todos no resultado com prototype nulo, porque nenhum deles depende da cadeia de prototypes. A falha só aparece quando um helper, muitas vezes lá no fundo de uma biblioteca utilitária ou de um template engine, chama um método no próprio objeto. Um grupo que silenciosamente nunca é renderizado, ou um TypeError lançado de dentro de um loop de renderização, é o sintoma típico.
Quais Navegadores Suportam Object.groupBy e Map.groupBy?
Object.groupBy e Map.groupBy compartilham a mesma linha de suporte: ambos estão marcados como Baseline Widely available no MDN, disponíveis em todos os navegadores desde março de 2024, então nenhum deles precisa de polyfill para os alvos de navegador atuais. A decisão entre eles se resume à chave: se o nome do grupo for naturalmente uma string (um status, uma categoria, o nome de um time), Object.groupBy entrega um objeto de aparência simples que você pode indexar com notação de ponto. Se a chave for um objeto, um Date, um número com o qual você fará aritmética, ou um booleano que você quer comparar como booleano, Map.groupBy o mantém intacto e evita as duas armadilhas acima.
Substituindo o Acumulador
Um reduce com uma linha ??= [] pode se tornar uma chamada de uma linha a Object.groupBy sempre que a chave de agrupamento for uma string e nada downstream chamar hasOwnProperty no resultado. Quando a chave for qualquer outra coisa, recorra a Map.groupBy e leia os grupos de volta com .get(). De qualquer forma, a lógica que decide a pertinência ao grupo é o único código que resta para testar.
Perguntas Frequentes
Object.groupBy funciona em TypeScript, e qual tipo ele retorna?
Sim. O TypeScript 5.4 adicionou declarações de tipo para Object.groupBy e Map.groupBy, disponíveis quando o target ou a lib do tsconfig inclui es2024 ou esnext; configurações de lib mais antigas informam que groupBy não existe em ObjectConstructor. Object.groupBy é tipado como um Partial Record, então todo grupo é possivelmente undefined e precisa de uma verificação antes de você indexá-lo. Map.groupBy é tipado como um Map do tipo da chave para um array de elementos.
O que acontece se o callback de Object.groupBy retornar undefined ou null?
O elemento cai em um grupo com a chave string 'undefined' ou 'null', porque Object.groupBy converte todo resultado de callback em uma property key. Nada é pulado e nenhum erro é lançado, então um campo ausente produz silenciosamente um grupo extra. Map.groupBy mantém o valor undefined ou null real como chave do Map. Para excluir esses elementos, filtre o array antes ou retorne uma chave de fallback como 'unknown'.
Qual é a diferença entre Object.groupBy e o groupBy do Lodash?
O groupBy do Lodash retorna um objeto comum que herda de Object.prototype, então hasOwnProperty funciona nele; Object.groupBy retorna um objeto com prototype nulo. O Lodash aceita um atalho com nome de propriedade, como groupBy(orders, 'status'), e chama um iteratee de função com um argumento, o valor, enquanto Object.groupBy exige uma função e passa o elemento e seu índice. O Lodash também aceita objetos comuns como entrada; Object.groupBy aceita qualquer iterável. Ambos coagem as chaves para strings.
Como agrupo por múltiplos campos com Object.groupBy?
Retorne uma única string composta a partir do callback, por exemplo juntando o status e um bucket de tamanho com um separador para produzir chaves como 'shipped:large'. Object.groupBy não tem modo de múltiplas chaves; cada elemento recebe exatamente uma property key. Se você precisar dos campos separadamente, aninhe as chamadas: agrupe primeiro por status, depois execute Object.groupBy no array de cada grupo para o segundo campo, o que gera uma estrutura de dois níveis lida como result.shipped.large.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k