Maps and Sets Are Underutilized
JS developers often default to objects and arrays as the go-to data structures. However, maps and sets are powerful alternatives that are frequently overlooked, especially in cases where developers reach for objects when a map would be more suitable or use arrays when a set is functionally better. These structures offer unique advantages, enabling highly efficient and expressive code when used appropriately.
Quick Note
While maps and sets are technically objects in JS—since nearly everything is—this post distinguishes them from what we traditionally refer to as objects: structures used for key-value pairs with string or symbol keys.
Think of a map as a more flexible object, capable of using any data type as a key. Similarly, a set can be thought of as a "no duplicates allowed" basket, perfect for collections where uniqueness matters.
Maps
A map is a collection of key-value pairs where both keys and values can be of any data type. Unlike traditional objects, which coerce keys to strings, map preserves the original data type of its keys, offering more flexibility:
It may not be immediately obvious, but the equivalent code for a plain object does not yield the same result. This is due to
key coercion, which means when we try to use an object as a key, we coerce it to a string using the object's toString()
method which
for many objects will be [object Object]
.
In addition to data type flexibility, maps possess other key features that make it a valuable data structure.
Maps preserve the insertion order of their entries, ensuring that the data they hold remains consistent and predictable, which can be useful for iteration.
Perhaps the most useful feature however, is that maps are very performant, and can quickly compute if a key exists in the collection with the has()
method, quickly return the value associated with that key with the get()
method, and quickly delete the pair with the delete()
method.
Map Use Case
Suppose we're building a real-time collaborative document editor in the browser similar to Google Docs, and we're tasked with building a system to track cursor positions for multiple users. Using an object, a reasonable representation of the data could be:
Although this structure would probably work fine, I'd argue that a map would be preferable here for a few reasons.
Depending on the nature of the document, users may frequently join and leave the document. Hence, we need to account for many add/remove operations to ensure a smooth UX, and as previously mentioned, maps have superior performance for frequent additions and deletion of entries:
Similarly, such an application would also likely need a mechanism to broadcast cursor positions and possibly do so in an ordered fashion.
Not only do maps maintain insertion order, but they also have built-in methods like forEach()
to handle iteration efficiently:
An additional point to consider when implementing this functionality is that user IDs could contain special characters that conflict with the object prototype, or unexpected coercion of numbers to strings for different types of user IDs. Although such scenarios would very likely be rare, it is still nonetheless an additional layer of complexity to consider that wouldn't exist with a map. In fact, it is generally considered safe to use user-provided keys and values with a map.
Sets
A set is a collection of unique values. It automatically ensures that no duplicate values exist in the collection:
Even though in the above snippet we added 1, 3, and the string "hello world" twice, the collection maintained a single copy of each. Hence, sets are ideal for scenarios where uniqueness is required. They can also accept an array directly into their constructor to transform it into a set:
In addition to enforcing uniqueness, sets are also very performant for checking if an item exists in the collection.
In fact, they far outperform arrays in this regard. To check for the existence of an item in a set, we can
use the has()
method, which will compute the result quickly. For an array, assuming we don't have the index ahead of time, we're left with the includes()
method which can
be quite slow for large data sets since it will have to linearly scan for the element.
Set Use Case
Lets return to the scenario of building a real-time collaborative document editor in the browser. Now, suppose we are tasked with building a system to track active users collaborating on the document. Initially, it may seem intuitive to use an array to handle this:
Similar to the above case with an object and a map, although an array would probably work fine here, I'd argue that a set would be better.
Recall that we need to account for cases where many users join and leave a document. If we look closely at the array code snippet above,
each time a user joins, we're adding them to the collection without checking if the user already has been added. Of course, this boils down to application
architechure, and we could certainly design our application in a way where we're certain this is called once-per-join. However, if we didn't have some mechanism for ensuring
this is called once-per-join, then checking if a user has already been added would necessitate the use of the includes()
method, which as mentioned above, is quite slow.
We can avoid that potential issue entirely by using a set:
With a set, we don't have to think about "enforcing uniqueness" and we can always direclty add to the collection and have peace of mind about value duplication.
Note that we also have efficient deletion. Thats not to imply we couldn't have efficient deletion with an array; we can if we know the index ahead of time since
we could always perform a swap and use the pop()
method, but that would at the very least require that we know ahead of time the index of what we intend to delete, which may require
an additional data structure.
Beyond accounting for when users join and leave a document, we may also want to check if a user is still connected to the document.
If we used an array, unless we know ahead of time the index of the particular user, we're left with a linear scan which could cause performance issues for
a document with many users. With a set, we can effectively make the performance concern disappear since checking for membership is a constant-time operation
with the has()
method:
Final Note
While objects and arrays are fundamental in JS and remain essential for many use cases, maps and sets provide powerful tools for solving specific problems more efficiently. By recognizing when to use these structures, you can write cleaner, more expressive, and performant code.
Next time you're deciding on a data structure, take a moment to consider whether a map or set might simplify your logic and elevate your application's performance.