Search code examples
javascriptarraysecmascript-6javascript-objects

ES6 Map object to group values


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.values())
}

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?


Solution

  • 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)})