Jump to content

We don't need Map

Map is an ECMAScript 2015 feature that allows storing key-value pairs in a collection, similar to plain objects, but with a few “advantages” such as:

  • The keys can be of any type, not just strings (even other Maps).
  • The keys are ordered.
  • It includes a bunch of methods to manipulate the collection.

It is a great tool but, sadly, an overly used one, and more often than not, it can or should be replaced by a plain object or an array.

Data interchange and serialization

I wrote this first because, from my point of view, this is one of the most important reasons to avoid Map. One of the main advantages of using plain objects in JavaScript is that we can easily convert them to and from JSON. However, we can’t serialize Map as JSON, nor can we parse it from JSON directly. This limitation means we need to use extra steps or libraries to handle Map when working with JSON data.

For example, if we have a Map object like this:

1
const map = new Map([
2
["foo", 0],
3
["bar", 1],
4
]);

We cannot simply use JSON.stringify to convert it to JSON:

1
JSON.stringify(map); // "{}"

Instead, we need to convert the Map object to an array of key-value pairs first:

1
JSON.stringify([...map]); // `[["foo", 0], ["bar", 1]]`

Similarly, if we have a JSON string like this:

1
const json = `[
2
["foo", 0],
3
["bar", 1],
4
]`;

We cannot simply use JSON.parse to convert it to a Map object:

1
JSON.parse(json); // [["foo", 0], ["bar", 1]]

Instead, we need to pass the parsed Array to the Map constructor:

1
new Map(JSON.parse(json)); // Map(2) { "foo" => 0, "bar" => 1 }

These extra steps are cumbersome, verbose, and prone to errors and inconsistencies. For example, if we have nested Maps or other non-serializable values in our Map object, we must handle them separately or use custom replacer and reviver functions.

On the other hand, plain objects can be easily serialized and parsed as JSON without any extra steps or libraries:

1
const object = { foo: 0, bar: 1 };
2
3
JSON.stringify(object); // `{"foo":0,"bar":1}`
4
JSON.parse(`{"foo":0,"bar":1}`); // { foo: 0, bar: 1 }

So this makes plain objects much more convenient and reliable for working with data serialization.

Reading data from a Map

Another disadvantage of using Map objects is that they have a different syntax for accessing values than plain objects. To get a value from a Map object by its key, we need to use the get method:

1
map.get("foo"); // 0

However, to get a value from a plain object by its key, we can use dot notation or bracket notation:

1
object.foo; // 0
2
object["foo"]; // 0

This is picky, until we consider that we also need to write stuff like deconstructing differently:

1
const { foo } = object; // foo = 0
2
3
// vs
4
5
const foo = map.get("foo"); // foo = 0

Or if we want to get more than one value:

1
const { foo, bar } = object; // foo = 0, bar = 1
2
3
// vs
4
5
const foo = map.get("foo"); // foo = 0
6
const bar = map.get("bar"); // bar = 1
7
8
// or 😬
9
10
const [foo, bar] = ["foo", "bar"].map(key => map.get(key)); // foo = 0, bar = 1
11
12
// or 🤦🏻
13
14
const { foo, bar } = Object.fromEntries(map); // foo = 0, bar = 1

And for spread syntax:

1
const shallowCopy = { ...object }; // shallowCopy = { foo: 0 , bar: 1 }
2
3
// vs
4
5
const shallowCopy = new Map([...map]); // shallowCopy = Map(2) { "foo" => 0, "bar" => 1 }

We are sacrificing a lot of readability and consistency just for the sake of using Map.

Creating a Map

Staying on the topic of syntax, creating a new Map with data on it is also less readable than creating a plain object:

1
const map = new Map([
2
["foo", 0],
3
["bar", 1],
4
]);
5
6
// Or even worse
7
const map = new Map().set("foo", 0).set("bar", 1);

Meanwhile, to create a new plain object, we can use curly braces and comma-separated key-value pairs:

1
const obj = { foo: 0, bar: 1 };

The performance

I’m not a person that usually makes this argument. I would take something readable over something performant any day. But in this case, we have already proven that readability could be better, and performance could be better too. This point is not a surprise if we compare plain objects with objects that include methods (set, get, has, etc.) and state (size, the key-value pairs, etc.).

Of course, there may be some cases where Maps have better performance than plain objects, such as when using non-string keys or when iterating over extensive collections. However, these cases are rare and specific and do not justify using Map over plain objects in general.

Immutability

Map favors mutation. We can work with “immutable Map,” but then the performance suffers even more. So when working with architectures based on immutability, such as React, Map becomes even less compelling. I already wrote about the problems introduced by mutation and how to avoid them, so I’ll no go into details. We can achieve an immutable Map, but for that to be performant, we need libraries such as Immer, and at that point, we are just adding even more complexity to our code.

The sorting argument

One common argument favoring Map is that it has ordered key-value pairs, while plain objects can’t guarantee that. However, my point generally is: If we are concerned about order, we have an object in JavaScript already for that, and we have had it for a long time, and it’s called Array.

Conclusion

For the most part, we can replace Map with plain objects and get the same results with less and more readable and maintainable code with better performance.

Map has its uses. It can be an excellent tool for handling some types of structures in memory in some scenarios, but my rule of thumb generally is to use it only when we need to use non-string keys for some reason (tho, more often than not, an array of tuples are good enough).