DEV Community

Cover image for MapPlus: a better "Map" makes your code easier to read...
Mike Talbot ⭐
Mike Talbot ⭐

Posted on • Edited on

MapPlus: a better "Map" makes your code easier to read...

TL;DR

I introduce MapPlus, a simple class that makes your code easier to read and ensures Maps are easier to reason with.

What is Map

In JavaScript, the Map is a very useful built-in class that creates an O(1) lookup between a key and a value.

const myMap = new Map()

for(const file of files) {
    const [,extension] = file.name.split(".")
    if(!myMap.has(extension)) {
        myMap.set(extension, [])
    }
    myMap.get(extension).push(file)
}

Enter fullscreen mode Exit fullscreen mode

You can use Maps for all sorts of things you are likely to do regularly:

  • Creating grouped lists of data, like the above example grouping by file extension

  • Aggregating data, like counting or summing values across a range of keys

const items = ['apple','apple','orange','banana','apple'];
const counts = new Map();
for (const item of items) {
  counts.set(item, (counts.get(item) || 0) + 1);
}
Enter fullscreen mode Exit fullscreen mode
  • Creating rapid lookups to be used in subsequent steps
const users = [
  {id:1,name:'A',role:'admin'},
  {id:2,name:'B',role:'user'},
  {id:3,name:'C',role:'user'}
];
const userMap = new Map();
for (const u of users) {
  userMap.set(u.id, u);
}
Enter fullscreen mode Exit fullscreen mode

Why use Map?

Map is preferred to using a simple object ({}) for a couple of reasons, so long as you don't have to store the result using a stringify:

  • It can take keys which are not strings
  • It's slightly faster than an Object even if you are using string keys

There can be a lot of boilerplate and mixed concerns though, if the object you are storing in the map needs construction, which is anything from a simple array to a complex object, this needs to be interspersed with the code that uses it.


const map = new Map()

for(const item of items) {
   if(!map.has(item.type)) {
       const newType = new Type(item.type, getInfoForType(item.type))
       map.set(item.type, newType)
   }
   map.get(item.type).doSomething(item)

}

Enter fullscreen mode Exit fullscreen mode

This "can" be ok, but it becomes harder to keep DRY if you need to update or initialise the value in multiple places.

For this reason I use a MapPlus class, which is an extension to Map that provides a missing key initialiser function that can be supplied to the constructor or as a second parameter to the get if the initialiser does need in context information beyond just the key.

The MapPlus Class

class MapPlus extends Map {
    constructor(missingFunction) {
        super()
        this.missingFunction = missingFunction
    }

    get(key, createIfMissing = this.missingFunction) {
        let result = super.get(key)
        if (!result && createIfMissing) {
            result = createIfMissing(key)
            if (result && result.then) {
                const promise = result.then((value) => {
                    super.set(key, value)
                    return value
                })
                super.set(key, promise)
            } else {
                super.set(key, result)
            }
        }
        return result
    }
}
Enter fullscreen mode Exit fullscreen mode

With this you can just do things like:

const map = new MapPlus(()=>[])

for(const item of items) {
    map.get(item.type).push(item)
}

Enter fullscreen mode Exit fullscreen mode

If the key is missing it will just make an empty array, but the loop itself is kept simple and clean, concerned only with making the list.

I often need two levels of this so I'll have maps defined like this:

const map = new MapPlus(()=>new MapPlus(()=>[]))
for(const item of items) {
   map.get(item.type).get(item.subType).push(item)
}
Enter fullscreen mode Exit fullscreen mode

The constructor function does get the key being used so we can also do:

const map = new MapPlus((type)=>new Type(type, getInfoForType(type))

for(const item of items) {
    map.get(item.type).doSomething(item)
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.