Sometimes, All You Need Is a Map

Photo by: Senior Airman Brittany A. Chase

It’s a little sad that one of my favorite JavaScript features shares a common name with another common and widely used feature. Everybody knows and loves the built-in array method map(), but when have they ever paid much attention to “Map”. Yeah, that’s right. Uppercase Map. They could have called it SuperSet or JackedArray, but instead they went with map.

So what is a Map? It’s a built-in Object class / data structure that rests somewhere between an array and an object. And if you’ve ever found yourself reaching for lodash, or using Object.keys() to iterate over the properties of an object, or perhaps just couldn’t make up your mind about whether to use an array or an object then you might next time want to consider reaching for a Map instead. My goal is to explain the Map, convince you that it’s not scary, and hopefully get you to use one soon.

Here’s how you create one:

const myMap = new Map();

One of the cool things about a JavaScript object is that you can’t add two identical properties to one without overwriting the previous value. This feature makes the base JavaScript Object pretty useful for many things e.g. storing data that has the same shape and setting them to a unique identifier like an ID for a user.

const myUsers = {
"87a6sg87as6gas786": {
name: "Bob"
created: "12-18-1985",
passion: "Christ"
},
"879asdf8as987dff7": {
name: "Cindy",
created: "01-01-1984",
passion: "Toast"
}
};

If these user objects were stored in, say, an array it would take a proportional amount of time to access the item your were after related to the array’s size. Cindy’s index might be all the way at the end (she’s one in a million after all) and you’d have to loop over the array in it’s entirety to find her ID and return her data.

However, if you had stored your users in an object then you’d simply ask the object for Cindy by her ID and she would appear instantly. This way of using an object is always called a dictionary. There are many variations and I won’t be touching on any of them. Because this article is about Maps.

Let’s consider a situation where you wanted to display this list of users in a public directory where all their passions are revealed. If you had went with the dictionary over an array here’s what your code might look like in React:

Object.keys(myUsers).map(id => {
const { name, created, passion } = myUsers[id];
return(
<div>
<p>Name: {name}</p>
<p>User Since: {created}</p>
<p>Passion: {passion}</p>
</div>
)
})

Ok, not so bad. We’re turning our object into an array of it’s keys (IDs in this case) and then mapping over them and accessing the key on each iteration to grab it’s value and produce our output.

Let’s consider another situation in which our users are stored somewhere else on a database. And every time we ask for them they return in the order in which the users joined. We might get a sorted array back from our server and throw it into an Object.

const users = [
{
id: 9087274931,
name: "Bobby",
created: new Date("1987-01-01")
},
{
id: 5435924862,
name: "Bob",
created: new Date("1988-01-01")
},
{
id: 6335229066,
name: "Rob",
created: new Date("1989-01-01")
},
{
id: 9117042227,
name: "Robert",
created: new Date("1990-01-01")
},
{
id: 4153490880,
name: "Bobert",
created: new Date("1991-01-01")
},
{
id: 3111193817,
name: "Boob",
created: new Date("1992-01-01")
},
{
id: 5626876870,
name: "Rabbi",
created: new Date("1993-01-01")
},
{
id: 2283842385,
name: "Robby",
created: new Date("1994-01-01")
},
{
id: 2108859545,
name: "Rahb",
created: new Date("1995-01-01")
},
{
id: 6990566606,
name: "Kohlrabi",
created: new Date("1996-01-01")
}
];
const userDictionary = {};
users.forEach(({id, ...rest}) => {
userDictionary[id] = rest;
});

Here we initialized a new object and looped over our sorted users array to create our dictionary. Not the most efficient way to create our dictionary, but it works. Now let’s try to use our old friend Object.keys() to iterate and return this mob of Bobs.

Object.keys(userDictionary).forEach(id=>{
console.log(userDictionary[id].created.getFullYear());
})
...console...
1994
1993
1991
1990
1986
1987
1988
1989
1992
1995

Well… that’s disappointing. Despite the fact that they appear to be ordered in the above object when we iterate these are not even kind of in any sort of order.

It seems our reliable array is inefficient for grabbing the correct user by ID and our fastidious dictionary doesn’t have a clue when any of it’s data appeared. An array is like your friend who’s amazing at remembering when something happened, but takes forever to tell you what actually happened. And an object is like your friend who’s really smart, but can only remember details if you ask the to produce exactly what you’re looking for.

But in life and programming we thankfully have other options.

It’s Map time 😏

const userMap = new Map();
users.forEach(({id, ...rest}) => {
userMap.set(id, rest);
});
userMap.forEach((value, key) => {
console.log(value.created.getFullYear());
});
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995

Let’s break down what happened. Instead of initializing an object we created a new Map(). Then just like with our object we iterated over the sorted array only this time we used the .set() method to add a key value pair with our id as the key and remaining user object values as the value. Then we called .forEach() on the map and console logged the years that our Rob variations joined our site. And the values came out the order that they went in! Suspiciously behaving like an array…

What if we want to find Kohlrabi. We know he’s at the end of this list. If this was an array we’d have to check to see if his ID matches the position in the array while we loop over it. With a Map not so much.

const kohlrabi = userMap.get(6990566606);
console.log(kohlrabi);

There’s a couple of things you might notice about this. The first thing is that we used the .get() method to access the value we were after. But we also used a number (not a string like we would have with a object/dictionary). In fact, we could have stored the keys as strings and accessed them that way if we wanted to. But our IDs returned from our database as numbers so we might as well keep them as numbers.

Map keys can actually be… anything!

Consider these potentially pointless yet interesting examples…

const otherMap = new Map();
const coolObject123 = {cool: 123};
otherMap.set(coolObject123, "Huh???");
const speak = () => console.log('Hi');
otherMap.set(speak, 'But why?');

Heck, if we wanted to be real show-offs we could even create a new Map, then set a value of a Map who’s key is a Map…

const anotherMap = new Map();
otherMap.set(anotherMap, new Map());

IDK why you would ever do this. But the point is… you can. You can also use booleans or any other type of value, but the crucial thing to remember is that Map keys like Object keys are unique. A Map won’t stop you from resetting it’s value like a Set will. But you can quickly ask a Map whether it has what you’re looking for.

const someVariable = "yep";
const map = new Map();
map.set(someVariable);
map.has(someVariable) // -> true

This is similar to the way you might ask an object if it has a certain property.

const object = {"1": 1, "2": 2,"3": 3};
object.hasOwnProperty("1"); // --> true

Ok, so I hope maps are starting to sound interesting, but what else can maps do. It seems like it might take a lot of effort to create a map. We’ll probably have to loop over that array of values and add them one by one right? The answer is, it depends…

At the heart of every map rests a 2-dimensional array of key value pairs. That means if the data you’re working with is already “iterable” and takes the required shape then you can easily initialize a map by passing in your data set directly as either a two-dimensional array of key-value array pairs or by using the Object.entries() method on an object. This of course will depend on how you’ve decided to store your data on your back-end or in localStorage.

const twoDimensionalArray = [
[ 9087274931,
{
name: "Bobby",
created: new Date("1987-01-01")
}
],
[ 6990566606,
{
name: "Kohlrabi",
created: new Date("1996-01-01")
}
]
];
const keyValues = Object.entries({
"9087274931":{
name: "Bobby",
created: new Date("1987-01-01")
},
"6990566606":{
name: "Kohlrabi",
created: new Date("1996-01-01")
}
});

Passing either of these into a new Map() e.g new Map(keyValues) will produce a map of those key values pairs.

The Iterator-rator

This is where things start to get fun with Map. Maps are iterable. They don’t have proper indexes like arrays and you ironically can’t use the .map array method on them. But if you need to loop over one to find a particular value you luckily have some options. First you need to return a Map Iterator by calling one of the many iterator methods. Your options are: values(), keys(), and entries(). The first two are self explanatory the last one is basically a combination of the two (and can actually be omitted as we’ll see).

const valueIterator = map.values();

Next you can use a for ... of loop to loop over your values.

for(let value of valueIterator){
console.log(value);
}

If you need keys and values you can use the .entries() method or just use the Map itself like so:

for(let entry of someMap){
const key = entry[0];
const value = entry[1];
console.log(key, value);
}

Additionally, you can call forEach() on a Map just like you would an array and get the keys/values. Take note that the key follows the value in the arguments passed to the callback. You can also pass a third argument to get the entries, but this seems like an edge case in terms of usefulness.

someMap.forEach((value, key) => {
console.log(value, key);
})

Since, a map is basically a 2 dimensional array of key value pairs the entries themselves are arrays. If you find yourself needing to quickly transform a Map into an array of either keys or values you can use the spread operator to dump them into an array.

const valueArray = [...someMap.values()];
const keyArray = [...someMap.values()];

So if you found yourself saying well, Ok maps are cool. But what if I need to know the index of a certain item in it like an array. Sadly there is no built-in way to achieve this. But, you can spread both values and keys into arrays of matching lengths. So it’s not hard to imagine how we might be able to come up with an answer to this problem using something like JavaScript’s built-in array method .indexOf()

class MapIndex extends Map {
indexOfKey(key){
return [...this.keys()].indexOf(key);
}
valueAt(index){
return [...this.values()][index];
}
}
const mapIndex = new MapIndex()
.set("8sdf9sf745", new Date("2018-09-09"))
.set("8asdf78asd", new Date("2018-09-10"))
.set("676sdf76sf", new Date("2018-09-11"))
.set("sd6f7sfsdf", new Date("2018-09-12"))
.set("dsftystfds", new Date("2018-09-13"));
console.log(mapIndex.indexOfKey("sd6f7sfsdf"));
console.log(mapIndex.valueAt(3));
// --> 3
// --> whatever date is there, just trust me it works

Whoa whoa whoa! Yeah, that’s right… maps are pretty awesome. What I did here was extend the base class of Map and create a new class of MapIndex. Then we simply added some new methods that spread our values or keys into an array and can call whatever arrays methods we want on it.

What if we take this idea of extending the Map class a bit further and add another “array-like” method….

class SpliceMap extends Map {
mapSplice(start, remove, key, value){
const entries = [...this.entries()];
entries.splice(start, remove, [key, value]);
this.clear();
entries.forEach(entry=>this.set(entry[0],entry[1]));
}
}
mapIndex.mapSplice(3, 0, "newID", new Date("2018-09-11"));

Ok, I know what you’re saying this isn’t the most performant way to mutate a 2D array, and you’d be right. You’d probably want to fallback to more tried and true methods for dealing with very large data sets. But, that was just an example to get your creative juices flowing. I’m sure you can find ways to extend maps for use with all sorts of things and in consort with algorithms and data structures.

So you see, maps are pretty flexible, very useful out of the box, and can offer us similar functionality to arrays and objects with a little creativity. What are some cool ways you can think of to use Maps?