Agrupar arrays en JavaScript con Object.groupBy
Object.groupBy en JavaScript agrupa arrays por clave, compara reduce y Map.groupBy, y explica la coerción a string y los resultados sin prototipo.
Object.groupBy(items, callback) agrupa un array en una sola llamada: ejecuta el callback una vez por elemento, usa el valor devuelto como nombre del grupo y retorna un objeto que contiene un array de elementos coincidentes bajo cada nombre.
Muchas bases de código todavía arrastran un reduce hecho a mano para esto, con las mismas líneas de código repetitivo del acumulador copiadas entre archivos, o una importación de Lodash que se mantiene viva por una única llamada a groupBy. Ninguna de las dos opciones es incorrecta, y ambas siguen funcionando; pero con el método nativo disponible, ninguna es necesaria.
Este artículo muestra la llamada nativa aplicada a un problema concreto (pedidos por estado), la compara con el reduce al que sustituye, explica cuándo Map.groupBy es la herramienta adecuada en su lugar y repasa los dos comportamientos que suelen causar problemas en producción: claves que se convierten silenciosamente en cadenas y un objeto resultante que no tiene hasOwnProperty.
Puntos clave
Object.groupByinvoca su callback con dos argumentos,(element, index), y usa el valor devuelto como clave del grupo.- Reemplazar un acumulador de
reduceporObject.groupByes un cambio de legibilidad, no de rendimiento; no hay motivo para esperar que se ejecute más rápido. - Usa
Map.groupBycuando la clave de agrupación no sea una cadena: un objeto, unDateo un número que necesites conservar como número. - Agrupar por un booleano o un número con
Object.groupByproduce las claves de cadena"true"y"40", porque toda clave se convierte en una clave de propiedad. - El objeto que devuelve
Object.groupBytiene un prototipo nulo, por lo queresult.hasOwnProperty(...)lanza unTypeError; usaObject.hasOwno copia el resultado con spread.
Agrupar pedidos por estado en tres líneas
Dado un array de objetos de pedido, Object.groupBy produce un objeto indexado por estado en una sola expresión, sin acumulador y sin comprobación de existencia.
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
La referencia de Object.groupBy() en MDN describe el contrato: el primer argumento es cualquier iterable, no solo un array, y el resultado incluye una propiedad por cada clave distinta. Los grupos aparecen en el orden en que se encontró su primer miembro. Los elementos dentro de cada grupo son los objetos originales, no copias, por lo que mutar byStatus.shipped[0] también muta orders[0].
¿Cómo funciona el callback de Object.groupBy?
El callback recibe dos argumentos, el elemento actual y su índice, y lo que devuelva se convierte en la clave de grupo para ese elemento. La definición de Object.groupBy en ECMA-262 especifica exactamente esos dos argumentos; no existe un tercer argumento con el “array completo” como ocurre con map o filter.
Como la clave se calcula, el callback no se limita a leer un campo. Cualquier expresión que produzca una cadena funciona, incluida una comparación o un bucket derivado del í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] }
Cada elemento cae exactamente en un grupo. Si dos elementos producen la misma clave, comparten un array en orden de inserción.
Group by en JavaScript: reduce frente a Object.groupBy
Reemplazar un acumulador de reduce por Object.groupBy elimina el objeto inicial, la comprobación de existencia por elemento, la asignación del array, el push y el return; lo que queda es la única línea que decide a qué grupo pertenece un elemento.
Esta es la versión que la mayoría de las bases de código ya contienen, escrita de la forma más compacta que permite la asignación lógica de anulación (nullish):
const byStatusReduce = orders.reduce((acc, order) => {
acc[order.status] ??= [];
acc[order.status].push(order);
return acc;
}, {});
Y el mismo resultado con el método nativo:
const byStatus = Object.groupBy(orders, (order) => order.status);
Ambos producen agrupaciones equivalentes. La diferencia está en lo que quien lee debe mantener en la cabeza. En la forma con reduce, la intención de agrupar se reparte entre un objeto semilla del acumulador, una asignación condicional, una mutación y un valor de retorno, y cualquiera de esos cuatro puntos puede estar sutilmente mal. En la forma nativa, lo único que queda por revisar es la función de clave.
Object.groupBy es un cambio de legibilidad, no de rendimiento. Ambos enfoques iteran la entrada una vez y asignan un array por grupo, y no hay motivo para esperar que la llamada nativa sea más rápida que un reduce bien escrito. Elígela porque elimina código repetitivo, no por un benchmark.
¿Cuándo deberías usar Map.groupBy en su lugar?
Usa Map.groupBy cuando la clave de agrupación no sea una cadena: un objeto, un Date o un número que necesites conservar como número. Map.groupBy() recibe el mismo callback de dos argumentos y solo difiere en su tipo de retorno: un Map cuyas claves son exactamente los valores que devolvió el 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
Las claves regresan como números, en orden de inserción, y se leen con .get(). El propio ejemplo de MDN en la página de Map.groupBy agrupa por identidad de objeto, que es el caso que un objeto literal no puede manejar en absoluto: dos objetos distintos con contenido idéntico siguen siendo distintos como claves de Map.
Object.groupBy | Map.groupBy | |
|---|---|---|
| Tipo de clave | Convertida a cadena o símbolo | Cualquier valor, tal cual |
| Tipo de retorno | Objeto con prototipo nulo | Map |
| Leer un grupo | result.shipped | result.get(key) |
| Orden de iteración de claves | Orden de inserción, salvo que las claves tipo entero se ordenan de forma ascendente | Orden de inserción |
¿Por qué Object.groupBy convierte las claves numéricas en cadenas?
Agrupar por un booleano o un número con Object.groupBy produce las claves de cadena "true" y "40", no los valores originales true y 40; Map.groupBy preserva los valores originales como claves del Map. Lo que sea que devuelva el callback tiene que acabar como clave de propiedad, así que todo lo que no sea ya una cadena o un símbolo se convierte a cadena en el proceso. Ese es el comportamiento normal de los objetos, pero sigue sorprendiendo a quien espera un resultado con claves booleanas.
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 (la búsqueda también convierte)
const byTotalObj = Object.groupBy(orders, (order) => order.total);
console.log(Object.keys(byTotalObj)); // ["15", "22", "40", "60"]
En el caso numérico ocurren dos cosas. Los totales se convirtieron en cadenas y regresaron ordenados de forma ascendente en lugar de en orden de inserción, porque las claves de propiedad tipo entero se enumeran en orden numérico ascendente antes que las demás claves de cadena. El código que itera el resultado esperando la secuencia original renderizará los grupos en el orden incorrecto sin lanzar ningún error.
Las repeticiones de sesión de errores de agrupación muestran con frecuencia exactamente esta forma: un encabezado de categoría que dice true en lugar de “Shipped”, o buckets que aparecen ordenados cuando los datos no lo estaban. La consola está limpia, la forma de los datos parece correcta en un log porque { true: [...] } se imprime igual tanto si la clave es un booleano como si es una cadena, y solo al ver la interfaz renderizada junto al recorrido del código se hace evidente la conversión.
¿Por qué hasOwnProperty lanza un error sobre un resultado de Object.groupBy?
El objeto que devuelve Object.groupBy tiene un prototipo nulo, por lo que result.hasOwnProperty("shipped") lanza un TypeError; usa Object.hasOwn(result, "shipped") o copia el resultado con sintaxis de spread si el código posterior espera un objeto ordinario. MDN documenta el valor de retorno como un objeto con prototipo nulo, lo que significa que nada de Object.prototype es accesible a través de él: ni hasOwnProperty, ni toString, ni 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); // funciona con normalidad
const plain = { ...byStatus }; // objeto ordinario con Object.prototype
plain.hasOwnProperty("shipped"); // true
MDN señala Object.hasOwn como el sustituto moderno de hasOwnProperty, y forma parte de Baseline “ampliamente disponible” desde marzo de 2022, así que puedes usarlo directamente. Object.keys, Object.entries, el operador in, JSON.stringify y el spread funcionan todos sobre el resultado con prototipo nulo, porque ninguno depende de la cadena de prototipos. El fallo solo aparece cuando un helper, a menudo en las profundidades de una librería de utilidades o de un motor de plantillas, invoca un método sobre el objeto mismo. Un grupo que silenciosamente nunca se renderiza, o un TypeError lanzado desde dentro de un bucle de renderizado, es el síntoma típico.
¿Qué navegadores admiten Object.groupBy y Map.groupBy?
Object.groupBy y Map.groupBy comparten la misma línea de soporte: ambos están marcados en MDN como Baseline “ampliamente disponible”, disponibles en todos los navegadores desde marzo de 2024, por lo que ninguno necesita un polyfill para los targets de navegador actuales. La decisión entre ambos depende de la clave: si el nombre del grupo es naturalmente una cadena (un estado, una categoría, el nombre de un equipo), Object.groupBy te da un objeto de apariencia simple que puedes indexar con notación de punto. Si la clave es un objeto, un Date, un número con el que harás aritmética o un booleano que quieres comparar como booleano, Map.groupBy lo conserva intacto y evita las dos trampas anteriores.
Reemplazar el acumulador
Un reduce con una línea ??= [] puede convertirse en una llamada de una sola línea a Object.groupBy siempre que la clave de agrupación sea una cadena y nada en el código posterior invoque hasOwnProperty sobre el resultado. Cuando la clave sea cualquier otra cosa, recurre a Map.groupBy y lee los grupos con .get(). En cualquier caso, la lógica que decide la pertenencia a un grupo es el único código que queda por probar.
Preguntas frecuentes
¿Funciona Object.groupBy en TypeScript y qué tipo devuelve?
Sí. TypeScript 5.4 añadió las declaraciones de tipos para Object.groupBy y Map.groupBy, disponibles cuando el target o lib de tsconfig incluye es2024 o esnext; configuraciones de lib más antiguas informan de que groupBy no existe en ObjectConstructor. Object.groupBy está tipado como un Partial Record, así que cada grupo es posiblemente undefined y requiere una comprobación antes de indexarlo. Map.groupBy está tipado como un Map desde el tipo de clave hacia un array de elementos.
¿Qué ocurre si el callback de Object.groupBy devuelve undefined o null?
El elemento acaba en un grupo con la clave de cadena 'undefined' o 'null', porque Object.groupBy convierte cada resultado del callback en una clave de propiedad. No se omite nada ni se lanza ningún error, así que un campo ausente produce silenciosamente un grupo adicional. Map.groupBy conserva el valor real undefined o null como clave del Map. Para excluir esos elementos, filtra el array primero o devuelve una clave alternativa como 'unknown'.
¿Cuál es la diferencia entre Object.groupBy y groupBy de Lodash?
El groupBy de Lodash devuelve un objeto ordinario que hereda de Object.prototype, por lo que hasOwnProperty funciona sobre él; Object.groupBy devuelve un objeto con prototipo nulo. Lodash acepta la forma abreviada con nombre de propiedad, como groupBy(orders, 'status'), e invoca el iteratee de función con un único argumento, el valor, mientras que Object.groupBy requiere una función y pasa el elemento y su índice. Lodash también acepta objetos planos como entrada; Object.groupBy acepta cualquier iterable. Ambos convierten las claves a cadenas.
¿Cómo agrupo por varios campos con Object.groupBy?
Devuelve una única cadena compuesta desde el callback, por ejemplo uniendo el estado y un bucket de tamaño con un separador para producir claves como 'shipped:large'. Object.groupBy no tiene un modo multiclave; cada elemento recibe exactamente una clave de propiedad. Si necesitas los campos por separado, anida las llamadas: agrupa primero por estado y luego ejecuta Object.groupBy sobre el array de cada grupo para el segundo campo, lo que produce una estructura de dos niveles que se lee 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