Array vs Set vs Object vs Map

JavaScript provides a variety of data structures for storing data. However, developers often tend to use Arrays and Objects to solve most problems without considering whether they are the appropriate data structure. The use of Sets instead of Arrays and Maps instead of Objects can provide numerous benefits. This document provides a guide on how to use Set and Map, as well as the reasons why they should be considered over Arrays and Objects.

Array and Set

Consider a list of unique user IDs that gets appended to by some external operation. It would be more efficient to manage this list using a Set rather than an Array since a Set doesn’t allow duplicate items. Below is an example of how Set works:

const userIds = [1, 2, 3, 4, 4, 7];
const userIdSet = new Set(userIds);

console.log(userIds); // [ 1, 2, 3, 4, 4, 7 ]
console.log(userIdSet); // Set(5) { 1, 2, 3, 4, 7 }

Adding Items

Adding a new item to an Array can be done using push to add items to the end of the Array or unshift to add them to the start of the Array. In terms of performance, using push appends an element to the end of the Array with constant time complexity or O(1). If unshift is used, an element with an index of 0 is added and all the other elements are shifted by 1, resulting in linear time complexity or O(n).

In contrast, Set only allows the add function to add an element to the end of the Set, providing it with constant time complexity.



console.log(userIds); // [ 17, 1, 2, 3, 4, 4, 7, 12 ]
console.log(userIdSet); // Set { 1, 2, 3, 4, 7, 12, 17 }

Accessing Items

Accessing values in Arrays and Sets differs greatly due to how they are stored in memory. Arrays are indexed collections, which means that they are stored sequentially by index. In contrast, Sets are keyed collections that use a hash table internally to store data using keys.

Therefore, Array elements can be accessed by index with O(1) time complexity. However, if we want to find an ID within the Array, we would have to iterate over the Array using find or findIndex, resulting in a time complexity of O(n). If we want to know if the Array contains a certain ID value, we can use the includes function, which also iterates through all the items of the Array.

For Sets, we only have access to the has function, which produces the same result as the includes function in Arrays. However, because it is a keyed collection, the has function has a constant time complexity or O(1).

console.log(userIds[6]); // 7
console.log(userIds.findIndex((id) => id === 7)); // 6

console.log(userIds.includes(7)); // true
console.log(userIdSet.has(7)); // true

Removing Items

Removing an item from an Array can be done using filter to filter out the matching items or splice to remove the matching item by index. The former method only iterates through the items once with a time complexity of O(n), while the latter method iterates through the items twice with a time complexity of O(n2).

For a Set, we can simply use the delete function to remove the matching item, providing it with a time complexity of O(1) due to it being a keyed collection.

function removeUserByIdSplice(id) {
		userIds.findIndex((userId) => userId === id),

function removeUserByIdFilter(id) {
	userIds = userIds.filter((userId) => userId !== id);

function removeUserByIdSet(id) {

When Should You Use an Array Over a Set?

Although there are numerous reasons to use a Set over an Array, sometimes having duplicate items in a list is desirable. Additionally, Arrays have many useful functions, such as filter, map, sort, and reduce, which allow us to perform operations on our collections.

Because a Set is iterable, it allows you to use the for..of construct or spread operator () to perform Array-like functions. Thus, you can easily convert a Set to an Array to make use of the additional functions an Array provides.

// from set to array
console.log([...userIdSet]); // [ 1, 2, 3, 4, 7, 12, 17 ]

Object and Map

While Arrays and Sets are used to manage lists of data, Objects and Maps are used to manage key-value pairs. Objects are another example of a data structure that developers often overuse when they should consider using a Map instead. One of the main cases where you would want to consider using a Map over an Object is when keys are being added or deleted frequently.

You can easily instantiate a new Map based on an Object using the following code:

const usersObject = {
	tim: { id: 12, city: 'Perth' },
const usersMap = new Map(Object.entries(usersObject));

console.log(usersObject); // { tim: { id: 12, city: 'Perth' } }
console.log(usersMap); // Map(1) { 'tim' => { id: 12, city: 'Perth' } }

Adding Key-Value Pairs

Adding to an Object or a Map has constant time complexity. However, there is a bit of a gotcha with the Map code below. If we run this in JavaScript, it wouldn’t complain. However, we might notice that henry doesn’t get added in the way we expect. Instead, it gets added as an object property to the Map, and hence it cannot be accessed using the Map get function.

usersObject['henry'] = { id: 17, city: 'Melbourne' };
usersMap['henry'] = { id: 17, city: 'Melbourne' }; // 🚩

console.log(usersObject); // { tim: { id: 12, city: 'Perth' }, henry: { id: 17, city: 'Melbourne' } }
console.log(usersMap); // Map { 'tim' => { id: 12, city: 'Perth' }, henry: { id: 17, city: 'Melbourne' } }

console.log(usersMap.henry); // { id: 17, city: 'Melbourne' }
console.log(usersMap.get('henry')); // undefined

Instead, we must use the set function to add a key-value pair to a Map.

usersMap.set('henry', { id: 17, city: 'Melbourne' });

console.log(usersMap); // Map { 'tim' => { id: 12, city: 'Perth' }, 'henry' => { id: 17, city: 'Melbourne' } }

If we try to do something similar to an Object in TypeScript, we encounter issues with the usersObject because it infers the type when it is instantiated. To work around this, we need to define the type as { [key: string]: { id: number, city: string } }.

// TypeScript
const usersObject = {
	tim: { id: 12, city: 'Perth' },
const usersMap = new Map(Object.entries(usersObject));

// 🚩 Property 'henry' does not exist on type '{ tim: { id: number; city: string; }; }'.
usersObject['henry'] = { id: 17, city: 'Melbourne' };
usersMap.set('henry', { id: 17, city: 'Melbourne' });

Accessing Key-Value Pairs

Again, accessing key-value pairs has a constant time complexity for both Objects and Maps. The get function is used to access a value from within a Map.

console.log(usersObject.henry); // { id: 17, city: 'Melbourne' }
console.log(usersMap.get('henry')); // { id: 17, city: 'Melbourne' }

Removing Key-Value Pairs

The delete operator is one of my least understood operators in JavaScript, so I tend to avoid it. I came across this brilliant article on Understanding delete that I highly recommend. Instead, when removing a property from an Object, I prefer to use the spread operator, as shown below.

delete usersObject.henry; // OR const { henry, ...newUsersObject } = usersObject;

console.log(usersObject); // { tim: { id: 12, city: 'Perth' } }
console.log(usersMap); // Map { 'tim' => { id: 12, city: 'Perth' } }

Thankfully, Maps have it easy using the delete function.

If we are using TypeScript, we would encounter the following error when trying to use the delete function on an Object because it is inferring that tim is a required property of the usersObject.

// TypeScript
const usersObject = {
	tim: { id: 12, city: 'Perth' },

// 🚩 The operand of a 'delete' operator must be optional.
delete usersObject['tim'];

Other reasons to consider using a Map

One of the interesting properties of a Map is that you’re not restricted to using string values as keys, unlike with Objects. Therefore, you could have some additional metadata included in your keys.

const userLocationMap = new Map();

const tim = { id: 12, firstName: 'Tim', lastName: 'Veletta' };
const henry = { id: 17, firstName: 'Henry', lastName: 'Jones' };

userLocationMap.set(tim, 'Perth');
userLocationMap.set(henry, 'Melbourne');

console.log(userLocationMap); // Map { { id: 12, firstName: 'Tim', lastName: 'Veletta' } => 'Perth', { id: 17, firstName: 'Henry', lastName: 'Jones' } => 'Melbourne' }

userLocationMap.get(tim); // 'Perth'
userLocationMap.get(henry); // 'Melbourne'

Another property of a Map that could be useful is that the order of elements is preserved based on the order of insertion. This is also true for Objects in later versions of the JavaScript standard. However, it is not guaranteed, so developers cannot rely on it. Like Sets, Maps are iterable, meaning they can use the for..of construct to iterate over the elements in the Map. Alternatively, the forEach function can be used to do this.

for (let userLocation of userLocationMap) {
	console.log(userLocation); // [ { id: 12, firstName: 'Tim', lastName: 'Veletta' }, 'Perth' ] [ { id: 17, firstName: 'Henry', lastName: 'Jones' }, 'Melbourne' ]

userLocationMap.forEach((location, user) => {
	console.log(`${user.firstName} lives in ${location}`); // Tim lives in Perth Henry lives in Melbourne

Each of these operations has a time complexity of O(n) because we are only iterating over the items once. However, trying to do the same thing with Objects requires converting the Object to a list using either Object.keys, Object.values, or Object.entries, which, in itself, is an O(n) operation. If you then need to do something with each of the values, you end up iterating over the items again, resulting in a time complexity of O(n2).

Map also has a handy size property, so you can always find out how many items are present. With an Object, you have to convert it to a list and get the length property from there.

Congratulations on making it this far! I hope you have learned more about Sets and Maps and will consider using them more in the future. Below are a few resources that were used in creating this post if you want to learn more.

MDN - Objects vs Maps

Time Complexities Of Common Array Operations In JavaScript

Time Complexity of Objects and Arrays

← Back to the blog