ES6 Map object to group values [closed]

I want to convert this array:

[{
  department: 'HR',
  person: 'Tom'
},{
  department: 'Finance',
  person: 'Peter'
},{
  department: 'HR',
  person: 'Jane'
}];

Into this one, grouping people by department and changing the keys

[{
  role: 'HR',
  people: ['Tom','Jane']
 },{
  role: 'Finance',
  people: ['Peter']
}]

I use this technique that have seem around and works really well, I am not really sure if it’s got a name.

const data = [{department: 'HR', person: 'Tom'},{department: 'Finance',person: 'Peter'},{department: 'HR',person: 'Jane'}];

function groupPeopleByDepartmentWithObj(data) {
  const obj = {};
  for (const { department, person} of data) {
    if (!obj[department]) {
      obj[department] = { role: department, people: []};
    }
    obj[department].people.push(person);
  }
  return Object.values(obj);
}

console.log(groupPeopleByDepartmentWithObj(data));
.as-console-wrapper { max-height: 100% !important; top: 0; }

Using the ES6 Map object I can do the same with this

const data = [{department: 'HR', person: 'Tom'},{department: 'Finance',person: 'Peter'},{department: 'HR',person: 'Jane'}];

function groupPeopleByDepartmentWithMap(data)  {

  const mapper = new Map()
  for (const { department, person} of data) {
    if (!mapper.has(department)) {
      mapper.set(department, { role: department, people: []}) 
    }
    mapper.get(department).people.push(person)
  }
  return Array.from(mapper, ([role, people]) => (role, people))
}

console.log(groupPeopleByDepartmentWithMap(data))
.as-console-wrapper { max-height: 100% !important; top: 0; }

Is this approach better in any way, is there another way to use Map to get the same result?

Answer

There’s a fundamental difference between object properties and keys in a key-value store.

A property is something you define at the coding time, it has a fixed name and a well-defined meaning. A property is similar to a variable or a function.

A key is something that is only known at the run time, it is dynamic and has no inherent meaning except being associated with some value. A key is like an array index.

In the past, javascript Objects were misused to emulate key-value stores, using property names as keys. This had several serious drawbacks

  • property names, and hence keys, can only be strings
  • properties are generally not ordered
  • key iteration is clumsy (hasOwnProperty etc)
  • since there’s no difference between properties and keys, a dynamic key can accidentally overwrite a defined property, and vice versa, an existing property can be mistaken for a key

The Map was invented to address these drawbacks specifically:

  • Map keys can be anything
  • Map keys are always in insertion order
  • key iteration is well defined
  • keys and properties live in separate namespaces and don’t overwrite each other

Therefore, if you need a key-value store, Map is always a better choice. Using generic Objects for this is a mistake.

As for the “better” way to use Maps for grouping, this is rather subjective. I’d prefer a more generic version

/// data: an Iterable
/// keyFn: a function which will be applied to each data item to obtain its group key
/// returns: a Map(key => [items])

// function groupBy<T, K>(data: Iterable<T>, keyFn: (x: T) => K): Map<K, T[]> 
//
function groupBy(data, keyFn) {
    let m = new Map();

    for (let x of data) {
        let k = keyFn(x);
        if (!m.has(k))
            m.set(k, []);
        m.get(k).push(x);
    }

    return m;
}

which can be used like this for the task at hand:

let result = [];

for (let [role, items] of groupBy(data, x => x.department))
    result.push({role, people: items.map(x => x.person)})

Leave a Reply

Your email address will not be published. Required fields are marked *