Grouping iterables into objects and maps

Grouping iterables into objects and maps

2 May 2024 ecma-2024 generators grouping iterables maps

One of the features that landed in ECMAScript 2024 was the ability to group synchronous iterables (e.g. arrays) into objects or maps.

Suppose we had an array of objects that denoted fruit and vegetables...

const food = [ {name: 'Apple', type: 'fruit'}, {name: 'Orange', type: 'fruit'}, {name: 'Carrot', type: 'veg'}, {name: 'Leek', type: 'veg'} ]

...and we wanted to group them by food type, i.e. two groups: "fruit" and "veg". In other words, this:

{ fruit: [{ name: "Apple", fruit: true }, { name: "Orange", fruit: true }], veg: [{ name: "Carrot", veg: true }, { name: "Leek", veg: true }] }

Previously, we might have done this:

const types = new Set(food.map(({type}) => type)); const grouped = Object.fromEntries([...types].map(type => [type, food.filter(obj => obj.type == type)] ));

Using sets is useful here so that we derive the unique group names, rather than multiple of the same name.

Now, that's a bit of a code-golf way of achieving this; more readable would be an explicit loop or some such.

Object.groupBy() 🔗

Now, though, it's much easier, via Object.groupBy() (MDN link). Here's the signature:

Object.groupBy(array, callback(object))

We can achieve the same result as above simply via:

const grouped = Object.groupBy( food, obj => obj.fruit ? 'fruit' : 'veg' );

Well that was nice and clean! Our callback's job is to iteratively receive each object and, for each, return a string. The string is then used as the group name (i.e. key).

Note that this function does not mutate the original array; it returns a new one. Also, the objects contained within the new array are references to those in the original array; they're not copies.

Example with another iterable 🔗

Remember this works not only with arrays but with any synchronous iterables - in other words, anything that can be used with an iterator statement such as spread syntax.

Let's try an example with another type of iterable, a generator.

If you're hazy on generators, an admittedly obscure JavaScript API, check out my in-depth guide.

//create our generator - it "yields" a value with each iteration function* generator() { yield 1; yield 10; yield 'foo'; yield 'bar' } //group it by yield type - number or string const grouped = Object.groupBy(...generator(), value => typeof value);

Notice the use of spread syntax (...) with our generator, to iterate its values. Without that, we'd simply be feeding a function reference to Object.groupBy(), and would get an error.

Sure enough, we end up with the desired result:

{ number: [ 1, 10 ], string: [ "foo", "bar" ] }

Map.groupBy() 🔗

What if we don't want our keys (i.e. group names) to be strings? What if we want them to be, say, symbols instead? Enter Map.groupBy() (MDN link), which works exactly the same as above except, as the name implies, it returns a map, meaning the keys can be anything.

const fruitSymbol = Symbol('fruit'); const vegSymbol = Symbol('veg'); const grouped = Map.groupBy( food, obj => obj.fruit ? fruitSymbol : vegSymbol );

So there you have it! I do find it odd that we got two functions for almost the same thing. Yes, one returns an object and one a map, but couldn't this have been dependant on whether your callback returned strings or non-strings? But anyway, I'm nit-pickng!

Did I help you? Feel free to be amazing and buy me a coffee on Ko-fi!