Neues Node.js-Buch
Alle Artikel

Group objects by direct property or by nested property

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

  • the reduce() method
  • the groupBy() function from recipe 21
  • the groupBy() function from recipe 22

Directions

  1. Given: the groupBy() function that takes a property name as parameter (based on recipe 21, but renamed to groupByPropertyName()) …

    const groupByPropertyName = (array, property) =>
       array.reduce((grouped, object) => {
         let value = object[property];
         grouped[value] = grouped[value] || [];
         grouped[value].push(object);
         return grouped;
       }, {});
  2. … and the groupBy() function that takes a function as parameter (based on recipe 22, but renamed to groupByFunction()).

    const groupByFunction = (array, fn) =>
      array.reduce((grouped, object) => {
        let value = fn(object);
        grouped[value] = grouped[value] || [];
        grouped[value].push(object);
        return grouped;
      }, {});
  3. 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);
  4. 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);
  5. For allowing this we simply can combine the two functions groupByPropertyName() and groupByFunction(). 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.
  6. The first thing is to define two functions isString() and isFunction() plus another helper function isOfType(), 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 the Object.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: the typeof-operator is not that precise.

  7. 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;
      }, {});
  8. … 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;
      }, {});
  9. … 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;
      }, {});
  10. 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.