Regrouper des tableaux en JavaScript avec Object.groupBy
Object.groupBy en JavaScript regroupe des tableaux par clé, compare reduce et Map.groupBy, et explique la coercition en chaîne et les résultats sans prototype.
Object.groupBy(items, callback) regroupe un tableau en un seul appel : la fonction de rappel est exécutée une fois par élément, la valeur retournée sert de nom de groupe, et la méthode renvoie un objet contenant, sous chaque nom, un tableau des éléments correspondants.
De nombreuses bases de code embarquent encore un reduce écrit à la main pour cet usage, avec les mêmes quelques lignes de code d’accumulateur copiées d’un fichier à l’autre, ou conservent un import Lodash pour un unique appel à groupBy. Ni l’une ni l’autre de ces approches n’est incorrecte, et toutes deux fonctionnent toujours ; mais maintenant que la méthode native est disponible, aucune n’est nécessaire.
Cet article présente l’appel natif sur un problème concret (des commandes regroupées par statut), le met en regard du reduce qu’il remplace, explique dans quels cas Map.groupBy est l’outil approprié, et détaille les deux comportements qui posent problème en production : des clés silencieusement converties en chaînes de caractères, et un objet résultat dépourvu de hasOwnProperty.
Points clés à retenir
Object.groupByappelle sa fonction de rappel avec deux arguments,(element, index), et utilise la valeur retournée comme clé de groupe.- Remplacer un accumulateur
reduceparObject.groupByaméliore la lisibilité, pas les performances ; il n’y a aucune raison de s’attendre à une exécution plus rapide. - Utilisez
Map.groupBylorsque la clé de regroupement n’est pas une chaîne de caractères : un objet, uneDate, ou un nombre que vous devez conserver en tant que nombre. - Regrouper par booléen ou par nombre avec
Object.groupByproduit les clés de type chaîne"true"et"40", car chaque clé est convertie en clé de propriété. - L’objet retourné par
Object.groupBya un prototype null :result.hasOwnProperty(...)lève donc uneTypeError; utilisezObject.hasOwnou copiez le résultat avec la syntaxe de décomposition.
Regrouper des commandes par statut en trois lignes
À partir d’un tableau d’objets commande, Object.groupBy produit un objet indexé par statut en une seule expression, sans accumulateur ni test d’existence.
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 référence Object.groupBy() sur MDN décrit le contrat : le premier argument est un itérable quelconque, pas nécessairement un tableau, et le résultat comporte une propriété par clé distincte. Les groupes apparaissent dans l’ordre où leur premier membre a été rencontré. Les éléments contenus dans chaque groupe sont les objets d’origine, et non des copies : modifier byStatus.shipped[0] modifie donc aussi orders[0].
Comment fonctionne la fonction de rappel d’Object.groupBy ?
La fonction de rappel reçoit deux arguments, l’élément courant et son index, et la valeur qu’elle retourne devient la clé de groupe de cet élément. La définition d’Object.groupBy dans ECMA-262 spécifie exactement ces deux arguments ; il n’existe pas de troisième argument « tableau complet » comme c’est le cas avec map ou filter.
Puisque la clé est calculée, la fonction de rappel ne se limite pas à la lecture d’un champ. Toute expression produisant une chaîne de caractères convient, y compris une comparaison ou un intervalle dérivé de l’index :
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] }
Chaque élément se retrouve dans un seul et unique groupe. Si deux éléments produisent la même clé, ils partagent un tableau, dans leur ordre d’insertion.
Group by en JavaScript : reduce vs Object.groupBy
Remplacer un accumulateur reduce par Object.groupBy élimine l’objet initial, le test d’existence par élément, l’allocation du tableau, le push et le return ; il ne reste que l’unique ligne qui détermine à quel groupe appartient un élément.
Voici la version que contiennent déjà la plupart des bases de code, écrite aussi succinctement que l’affectation logique avec coalescence des nuls le permet :
const byStatusReduce = orders.reduce((acc, order) => {
acc[order.status] ??= [];
acc[order.status].push(order);
return acc;
}, {});
Et le même résultat avec la méthode native :
const byStatus = Object.groupBy(orders, (order) => order.status);
Les deux produisent des regroupements équivalents. La différence réside dans ce que le lecteur doit garder en tête. Dans la forme reduce, l’intention de regroupement est répartie entre un objet initial, une allocation conditionnelle, une mutation et une valeur de retour, et chacun de ces quatre éléments peut être subtilement erroné. Dans la forme native, la seule chose à relire est la fonction de clé.
Object.groupBy est un gain de lisibilité, pas de performance. Les deux approches parcourent l’entrée une seule fois et allouent un tableau par groupe ; il n’y a aucune raison de s’attendre à ce que l’appel natif soit plus rapide qu’un reduce bien écrit. Choisissez-le parce qu’il supprime du code répétitif, pas à cause d’un benchmark.
Quand faut-il utiliser Map.groupBy à la place ?
Utilisez Map.groupBy lorsque la clé de regroupement n’est pas une chaîne de caractères : un objet, une Date, ou un nombre que vous devez conserver en tant que nombre. Map.groupBy() accepte la même fonction de rappel à deux arguments et ne diffère que par son type de retour : une Map dont les clés sont exactement les valeurs retournées par la fonction de rappel.
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
Les clés sont restituées sous forme de nombres, dans leur ordre d’insertion, et se lisent avec .get(). L’exemple de MDN sur la page Map.groupBy regroupe par identité d’objet, un cas qu’un objet littéral ne peut absolument pas traiter : deux objets distincts au contenu identique restent distincts en tant que clés de Map.
Object.groupBy | Map.groupBy | |
|---|---|---|
| Type de clé | Converti en chaîne ou en symbole | Toute valeur, conservée telle quelle |
| Type de retour | Objet à prototype null | Map |
| Lecture d’un groupe | result.shipped | result.get(key) |
| Ordre d’itération des clés | Ordre d’insertion, sauf les clés de type entier triées par ordre croissant | Ordre d’insertion |
Pourquoi Object.groupBy transforme-t-il les clés numériques en chaînes ?
Regrouper par booléen ou par nombre avec Object.groupBy produit les clés de type chaîne "true" et "40", et non les valeurs d’origine true et 40 ; Map.groupBy préserve quant à lui les valeurs d’origine comme clés de Map. Tout ce que retourne la fonction de rappel doit finir par devenir une clé de propriété : ce qui n’est pas déjà une chaîne ou un symbole est donc converti en chaîne au passage. C’est le comportement ordinaire des objets, mais il surprend malgré tout quiconque s’attend à un résultat indexé par booléens.
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 lecture convertit aussi)
const byTotalObj = Object.groupBy(orders, (order) => order.total);
console.log(Object.keys(byTotalObj)); // ["15", "22", "40", "60"]
Deux phénomènes se produisent dans le cas numérique. Les totaux sont devenus des chaînes, et ils sont restitués triés par ordre croissant plutôt que dans leur ordre d’insertion, car les clés de propriété de type entier sont énumérées par ordre numérique croissant avant les autres clés de type chaîne. Un code qui parcourt le résultat en s’attendant à la séquence d’origine affichera les groupes dans le mauvais ordre, sans lever la moindre erreur.
Les relectures de session portant sur des bugs de regroupement présentent fréquemment exactement ce profil : un en-tête de catégorie affichant true au lieu de « Expédiée », ou des ensembles apparaissant triés alors que les données ne l’étaient pas. La console est vierge, la structure des données paraît correcte dans les logs puisque { true: [...] } s’affiche de façon identique que la clé soit un booléen ou une chaîne, et seule la confrontation de l’interface rendue avec le chemin d’exécution rend la conversion évidente.
Pourquoi hasOwnProperty lève-t-il une erreur sur un résultat d’Object.groupBy ?
L’objet retourné par Object.groupBy a un prototype null : result.hasOwnProperty("shipped") lève donc une TypeError ; utilisez Object.hasOwn(result, "shipped"), ou copiez le résultat avec la syntaxe de décomposition si le code en aval attend un objet ordinaire. MDN documente la valeur de retour comme un objet à prototype null, ce qui signifie que rien de Object.prototype n’est accessible à travers lui : 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); // fonctionne normalement
const plain = { ...byStatus }; // objet ordinaire avec Object.prototype
plain.hasOwnProperty("shipped"); // true
MDN désigne Object.hasOwn comme le substitut moderne de hasOwnProperty ; cette méthode est classée Baseline « largement disponible » depuis mars 2022, vous pouvez donc y recourir directement. Object.keys, Object.entries, l’opérateur in, JSON.stringify et la décomposition fonctionnent tous sur un résultat à prototype null, car aucun d’eux ne dépend de la chaîne de prototypes. La défaillance n’apparaît que lorsqu’une fonction utilitaire, souvent enfouie au cœur d’une bibliothèque ou d’un moteur de templates, appelle une méthode sur l’objet lui-même. Un groupe qui n’est silencieusement jamais rendu, ou une TypeError levée depuis l’intérieur d’une boucle de rendu, en constituent les symptômes typiques.
Quels navigateurs prennent en charge Object.groupBy et Map.groupBy ?
Object.groupBy et Map.groupBy partagent le même niveau de prise en charge : tous deux sont classés Baseline « largement disponible » sur MDN, disponibles dans l’ensemble des navigateurs depuis mars 2024 ; aucun ne nécessite donc de polyfill pour les cibles de navigateurs actuelles. Le choix entre les deux dépend de la clé : si le nom de groupe est naturellement une chaîne de caractères (un statut, une catégorie, un nom d’équipe), Object.groupBy vous fournit un objet d’apparence ordinaire que vous pouvez indexer avec la notation par point. Si la clé est un objet, une Date, un nombre sur lequel vous effectuerez des opérations arithmétiques, ou un booléen que vous souhaitez comparer en tant que booléen, Map.groupBy la conserve intacte et évite les deux pièges évoqués plus haut.
Remplacer l’accumulateur
Un reduce comportant une ligne ??= [] peut devenir un appel Object.groupBy d’une seule ligne dès lors que la clé de regroupement est une chaîne de caractères et qu’aucun code en aval n’appelle hasOwnProperty sur le résultat. Lorsque la clé est d’un autre type, tournez-vous vers Map.groupBy et lisez les groupes avec .get(). Dans les deux cas, la logique qui détermine l’appartenance à un groupe est le seul code qu’il reste à tester.
FAQ
Object.groupBy fonctionne-t-il en TypeScript, et quel type retourne-t-il ?
Oui. TypeScript 5.4 a ajouté les déclarations de types pour Object.groupBy et Map.groupBy, disponibles lorsque l'option target ou lib du tsconfig inclut es2024 ou esnext ; des réglages de lib plus anciens signalent que groupBy n'existe pas sur ObjectConstructor. Object.groupBy est typé comme un Partial Record : chaque groupe est donc potentiellement undefined et nécessite une vérification avant toute indexation. Map.groupBy est typé comme une Map allant du type de la clé vers un tableau d'éléments.
Que se passe-t-il si la fonction de rappel d'Object.groupBy retourne undefined ou null ?
L'élément se retrouve dans un groupe dont la clé est la chaîne 'undefined' ou 'null', car Object.groupBy convertit chaque résultat de la fonction de rappel en clé de propriété. Rien n'est ignoré et aucune erreur n'est levée : un champ manquant produit donc silencieusement un groupe supplémentaire. Map.groupBy conserve la valeur undefined ou null réelle comme clé de Map. Pour exclure ces éléments, filtrez d'abord le tableau ou retournez une clé de repli telle que 'unknown'.
Quelle est la différence entre Object.groupBy et le groupBy de Lodash ?
Le groupBy de Lodash retourne un objet ordinaire qui hérite de Object.prototype : hasOwnProperty y fonctionne donc ; Object.groupBy retourne un objet à prototype null. Lodash accepte un raccourci sous forme de nom de propriété, comme groupBy(orders, 'status'), et appelle un itératee de type fonction avec un seul argument, la valeur, tandis qu'Object.groupBy exige une fonction et lui transmet l'élément ainsi que son index. Lodash accepte également des objets simples en entrée ; Object.groupBy accepte tout itérable. Les deux convertissent les clés en chaînes de caractères.
Comment regrouper selon plusieurs champs avec Object.groupBy ?
Retournez une unique chaîne composite depuis la fonction de rappel, par exemple en joignant le statut et un intervalle de taille avec un séparateur, pour produire des clés du type 'shipped:large'. Object.groupBy ne propose pas de mode multi-clés ; chaque élément reçoit exactement une clé de propriété. Si vous avez besoin des champs séparément, imbriquez les appels : regroupez d'abord par statut, puis exécutez Object.groupBy sur le tableau de chaque groupe pour le second champ, ce qui produit une structure à deux niveaux lue comme 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