Problem
You have an array of objects and want to group them either by a direct property or by a nested property (i.e., a combination of recipe 21 and recipe 22).
Ingredients
Directions
-
Given: the
groupBy()
function that takes a property name as parameter (based on recipe 21, but renamed togroupByPropertyName()
) …const groupByPropertyName = (array, property) => array.reduce((grouped, object) => { let value = object[property]; grouped[value] = grouped[value] || []; grouped[value].push(object); return grouped; }, {});
-
… and the
groupBy()
function that takes a function as parameter (based on recipe 22, but renamed togroupByFunction()
).const groupByFunction = (array, fn) => array.reduce((grouped, object) => { let value = fn(object); grouped[value] = grouped[value] || []; grouped[value].push(object); return grouped; }, {});
-
The second variant (
groupByFunction()
) is of course more powerful, since you can group objects by any criterion, e.g., a direct property or a nested property:let persons = [ { firstName: 'John', lastName: 'Doe', address: { city: 'London' } }, { firstName: 'Jane', lastName: 'Doe', address: { city: 'Birmingham' } }, { firstName: 'Jane', lastName: 'Smith', address: { city: 'Birmingham' } }, { firstName: 'Dave', lastName: 'Smith', address: { city: 'London' } }, { firstName: 'Jane', lastName: 'Carpenter', address: { city: 'Birmingham' } } ] let groupedByFirstName = groupByFunction(persons, person => person.firstName); let groupedByCity = groupByFunction(persons, person => person.address.city);
-
However it would be nice if in case of a direct property we simply could pass the name of that property instead of a function (so that we again have just one function named
groupBy()
).let groupedByFirstName = groupBy(array, 'firstName'); let groupedByCity = groupBy(persons, person => person.address.city);
- For allowing this we simply can combine the two functions
groupByPropertyName()
andgroupByFunction()
. The plan: check if the parameter is a string (i.e., a property name) or if it is a function and apply the appropriate logic. -
The first thing is to define two functions
isString()
andisFunction()
plus another helper functionisOfType()
, so that we can check the types.const isOfType = type => x => Object.prototype.toString.call(x) === type const isString = x => isOfType('[object String]')(x) const isFunction = x => isOfType('[object Function]')(x)
Note: the best and most effective way of checking the type of something is not the
typeof
operator, but the usage of theObject.prototype.toString
method as shown in the listing above. The detailed reason for this will be explained in a later recipe, when we handle type checking in JavaScript, but for the moment you can remember: thetypeof
-operator is not that precise. -
Now we change the
groupBy()
function to check the parameter with the help of the type-checking functions just created. If the parameter is a string, get the property of the particular object with that name …const groupBy = (array, x) => array.reduce((grouped, object) => { let value = ''; if(isString(x)) { value = object[x]; } ... grouped[value] = grouped[value] || []; grouped[value].push(object); return grouped; }, {});
-
… else if it is a function, call that function passing the particular object …
const groupBy = (array, x) => array.reduce((grouped, object) => { let value = ''; if(isString(x)) { value = object[x]; } else if(isFunction(x)) { value = x(object); } ... grouped[value] = grouped[value] || []; grouped[value].push(object); return grouped; }, {});
-
… and in case nothing of that is true throw a type error.
const groupBy = (array, x) => array.reduce((grouped, object) => { let value = ''; if(isString(x)) { value = object[x]; } else if(isFunction(x)) { value = x(object); } else { throw new TypeError('String or function expected'); } grouped[value] = grouped[value] || []; grouped[value].push(object); return grouped; }, {});
-
Voilá, now we have a function that is able to group objects either by a property name or by a function.
const isOfType = type => x => Object.prototype.toString.call(x) === type const isString = x => isOfType('[object String]')(x) const isFunction = x => isOfType('[object Function]')(x) const groupBy = (array, x) => array.reduce((grouped, object) => { let value = ''; if(isString(x)) { value = object[x]; } else if(isFunction(x)) { value = x(object); } else { throw new TypeError('String or function expected'); } grouped[value] = grouped[value] || []; grouped[value].push(object); return grouped; }, {}); let groupedByFirstName = groupBy(persons, 'firstName'); let groupedByCity = groupBy(persons, person => person.address.city);
Notes
- Type checking is a complex topic in JavaScript. We will discuss this in another recipe.