Agrupando um array com groupBy - JavaScript

Neste texto, vamos explorar o conceito de agrupamento de dados e comparar as abordagens reduce e groupBy do JavaScript. Também é abordado o conceito de polyfill e o uso do groupBy no TypeScript.

Agrupando um array com groupBy - JavaScript
Agrupando um array com groupBy - JavaScript

Agrupar dados em JavaScript costumava ser uma tarefa que exigia um pouco mais de esforço. Uma das abordagens mais comuns era usar o método reduce. Recentemente, em julho de 2024, foi lançada no ES2024 (referência abaixo) a especificação para o método nativo groupBy. Apesar de ser recente, atualmente a maioria dos navegadores já oferece suporte ao groupBy, tornando essa tarefa (agrupar dados) muito mais simples.

Neste texto, abordaremos como agrupar dados usando tanto reduce quanto groupBy. Além disso, discutiremos seu suporte no TypeScript.

ECMAScript 2024, the 15th edition, added facilities for resizing and transferring ArrayBuffers and SharedArrayBuffers; added a new RegExp /v flag for creating RegExps with more advanced features for working with sets of strings; and introduced the Promise.withResolvers convenience method for constructing Promises, the Object.groupBy and Map.groupBy methods for aggregating data, the Atomics.waitAsync method for asynchronously waiting for a change to shared memory, and the String.prototype.isWellFormed and String.prototype.toWellFormed methods for checking and ensuring that strings contain only well-formed Unicode.

Link: https://tc39.es/ecma262/

O que é o agrupamento de dados?

Imagine que você tem uma lista de produtos, cada um com suas características. O agrupamento de dados é como organizar essa lista em grupos menores, com base em características em comum. Por exemplo, você poderia agrupar todos os produtos da categoria "eletrônicos", ou todos os produtos com preço acima de R$100.

// Lista de dados:
[
 {
  "id": 1,
  "name": "Notebook Dell",
  "category": "Computadores",
  "price": 4500.0
 },
 {
  "id": 2,
  "name": "Mouse Logitech",
  "category": "Periféricos",
  "price": 150.0
 },
 {
  "id": 3,
  "name": "Teclado Mecânico",
  "category": "Periféricos",
  "price": 350.0
 }
]

// Dados agrupados por categoria:
{
 "Computadores": [
  { "id": 1, "name": "Notebook Dell", "price": 4500.0 }
 ],
 "Periféricos": [
  { "id": 2, "name": "Mouse Logitech", "price": 150.0 },
  { "id": 3, "name": "Teclado Mecânico", "price": 350.0 }
 ]
}

Exemplo de agrupamento

Agrupando dados com o reduce

Antes do suporte a função groupBy, uma das alternativas era o uso do método reduce para agrupar dados, que é uma função mais antiga, já com suporte amplo.

Se você nunca utilizou o método reduce, pense nele como um liquidificador: você coloca diversos ingredientes e aperta o botão. No final, você tem um único produto: um suco. Basicamente, o reduce "reduz" um array a um único valor. Ele faz isso aplicando uma função a cada elemento do array, acumulando o resultado a cada passo.

// ***********************************
// Exemplo de como somar valores
// utilizando o reduce
// ***********************************
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((accumulator, currentValue) => {
  return accumulator + currentValue;
}, 0); // O valor inicial é 0

console.log(sum); // Imprime 10
Animação de como funciona as iterações do reduce
Animação de como funciona as iterações do reduce

Devido a natureza de funcionamento do reduce, podemos aproveitá-la para agrupar uma lista. Veja no exemplo abaixo o agrupamento de uma lista de transações por dia/mês:

const transacoes = [
  { data: new Date('2023-07-01T08:00:00.000-0300'),
    valor: 150.5,
    descricao: 'Pagamento de serviços' },
  { data: new Date('2023-07-01T12:00:00.000-0300'),
    valor: -10,
    descricao: 'Almoço' },
  { data: new Date('2023-07-05T12:00:00.000-0300'),
    valor: -30.0,
    descricao: 'Almoço com colegas' },
  { data: new Date('2023-07-05T19:00:00.000-0300'),
    valor: -45.75,
    descricao: 'Supermercado' },
  { data: new Date('2023-07-15T08:00:00.000-0300'),
    valor: 1200.0,
    descricao: 'Salário recebido' },
  { data: new Date('2023-07-20T10:00:00.000-0300'),
    valor: -100.0,
    descricao: 'Conta de luz' },
];

const transacoesAgrupadas = transacoes.reduce(
  (dadosAgrupados, transacao) => {
    const { data, valor, descricao } = transacao;

    const dd = data.getDate().toString()
      .padStart(2, '0');
    const mm = data.getMonth().toString()
      .padStart(2, '0');

    const chave = `${dd}-${mm}`;

    if (!dadosAgrupados.hasOwnProperty(chave)) {
      dadosAgrupados[chave] = [];
    }

    dadosAgrupados[chave].push({ valor, descricao });
    return dadosAgrupados;
  },
  {}
);

console.log(transacoesAgrupadas);

// ********************************
// Resultado:
// ********************************
const resultado = {
  '01-06': [
    { descricao: 'Pagamento de serviços', valor: 150.5 },
    { descricao: 'Almoço', valor: -10 },
  ],
  '05-06': [
    { descricao: 'Almoço com colegas', valor: -30 },
    { descricao: 'Supermercado', valor: -45.75 },
  ],
  '15-06': [
    { descricao: 'Salário recebido', valor: 1200 },
  ],
  '20-06': [
    { descricao: 'Conta de luz', valor: -100 },
  ],
};

Agrupando dados com o groupBy

A função groupBy é bem recente e faz parte do ECMAScript 2024. Apesar de recente, a maioria dos navegadores já dá suporte a ela em suas versões mais atuais, inclusive o Node.js na versão 21.

Lista de navegadores e suas respectivas primeiras versões que dão suporte ao método groupBy
magem gerada em julho de 2024 a partir do conteúdo do link: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy
As per usual a new version of the V8 engine is included in Node.js (updated to version 11.8, which is part of Chromium 118) bringing improved performance and new language features including:

Array grouping
ArrayBuffer.prototype.transfer
WebAssembly extended-const expressions

Link: https://nodejs.org/en/blog/announcements/v21-release-announce#v8-118

O uso do groupBy é bem simples e intuitivo, veja no exemplo abaixo:

const transacoesAgrupadas = Object.groupBy(
  // Lista que será agrupada:
  transacoes,

  // Função que retornará a "chave" de
  // agrupamento para cada item. Neste
  // exemplo a chave é a string "dia mês".
  (transacao) => {
    const { data } = transacao;
    const dd = data.getDate().toString()
      .padStart(2, "0");
    const mm = data.getMonth().toString()
      .padStart(2, "0");

    return `${dd}-${mm}`;
  }
);

Utilizando Polyfill para navegador antigos

Caso o local (runtime) onde o seu código será executado não dê suporte ao groupBy, você poderá recorrer a uma polyfill. No contexto do JavaScript, polyfill é um pedaço de código que adiciona uma funcionalidade a um navegador ou ambiente de execução que não a suporta nativamente. Em outras palavras, é como se você estivesse "preenchendo" uma lacuna de funcionalidade.

Exemplo de Polyfill:

Object.groupBy ??= function groupBy (iterable, callbackfn) {
  const obj = Object.create(null)
  let i = 0
  for (const value of iterable) {
    const key = callbackfn(value, i++)
    key in obj ? obj[key].push(value) : (obj[key] = [value])
  }
  return obj
}

Inclusão do groupBy no TypeScript

Para projetos TypeScript, como o Angular, a versão 5.4 introduziu as declarações de tipo para os de métodos Object.groupBy e Map.groupBy do JavaScript.

💡
TypeScript é um superconjunto do JavaScript que adiciona tipagem estática e outros recursos para tornar o desenvolvimento de aplicações JavaScript mais seguro e escalável. Seu produto final é um código JavaScript. Inclusive abordamos este assunto em detalhes neste link.

Portanto, caso tenha um projeto TypeScript onde precise utilizar o groupBy, para não pegar o erro abaixo, configure a lib que fica no arquivo tsconfig.json para o ESNext, já que na data corrente (julho/2024) não há a opção ES2024:

Property 'groupBy' does not exist on type 'ObjectConstructor'.ts(2339)

VS Code mostrando o erro de TypeScript por não encontrar a função groupBy. Na segunda imagem o erro foi corrigido
A primeira imagem mostra o erro e a segunda o ajuste feito na propriedade "lib" comentada logo acima
💡
A propriedade lib especifica quais bibliotecas de tipos (.d.ts) devem ser incluídas no projeto. Isso determina quais tipos e interfaces estarão disponíveis para o projeto TypeScript. Por exemplo, se você incluir "dom" em lib, o TypeScript terá conhecimento das interfaces do DOM (Document Object Model), como HTMLElementEvent, etc.

Considerações Finais

Embora seja recente, acredito que a função groupBy será amplamente utilizada. Eu mesmo já usei reduce várias vezes para agrupar dados, e com o groupBy, isso ficou muito mais simples.

Experimente implementar o groupBy em seus projetos e veja como ele pode simplificar seu código e melhorar a eficiência do seu trabalho.

Links interessantes: