Difficulty: Intermediate
Map and Set are keyed collection data structures introduced in ES6 that address specific limitations of plain objects and arrays. While objects and arrays remain the workhorses of JavaScript, Map and Set provide cleaner APIs and better performance characteristics for certain use cases. Understanding when to reach for these collections over their traditional counterparts is an important skill.
A Map is an ordered collection of key-value pairs where keys can be of any type - objects, functions, numbers, even NaN. This is a fundamental advantage over plain objects, which coerce all keys to strings. Map maintains insertion order, provides a size property for O(1) length checking, and offers methods like set(), get(), has(), delete(), and clear(). The forEach() method and entries(), keys(), values() iterators make Map fully iterable.
Choosing between Map and Object depends on your use case. Use Map when your keys are not strings (e.g., DOM elements, objects), when you need to frequently add and remove entries, when you need to know the size, or when iteration order matters. Use Object when your keys are strings and you need JSON serialization, when you want to leverage destructuring and spread syntax, or when you need prototype-based patterns. In performance-critical code, Map is generally faster than Object for frequent insertions and deletions.
A Set is a collection of unique values - any duplicate values are automatically ignored. Like Map, Set accepts values of any type and maintains insertion order. The add() method returns the Set itself (enabling chaining), has() checks for membership in O(1) time, and delete() removes a value. Set is the go-to solution for removing duplicates from arrays with [...new Set(array)].
Set methods like add, has, and delete provide clear, intention-revealing names compared to array alternatives. Checking if an array contains a value requires indexOf() or includes(), both O(n) operations. Set.has() performs the same check in O(1) time. For large collections where you need frequent membership checks, Set dramatically outperforms arrays.
Converting between Map/Set and arrays is straightforward using spread syntax or Array.from(). An array of [key, value] pairs can be passed to the Map constructor, and spreading a Map produces that same array format. Similarly, any iterable can be passed to the Set constructor, and spreading a Set produces an array of unique values. These conversions enable you to use array methods (map, filter, reduce) on Map and Set data, combining the strengths of both collection types.
// Creating and populating a Map
const userRoles = new Map();
userRoles.set('alice', 'admin');
userRoles.set('bob', 'editor');
userRoles.set('charlie', 'viewer');
console.log('Size:', userRoles.size);
console.log('Alice:', userRoles.get('alice'));
console.log('Has bob:', userRoles.has('bob'));
// Any type as key
const objKey = { id: 1 };
const funcKey = () => {};
const metadata = new Map();
metadata.set(objKey, 'object metadata');
metadata.set(funcKey, 'function metadata');
metadata.set(42, 'number key');
metadata.set(NaN, 'even NaN works');
console.log('\nObject key:', metadata.get(objKey));
console.log('NaN key:', metadata.get(NaN));
// Initialize from array of pairs
const config = new Map([
['host', 'localhost'],
['port', 3000],
['debug', true]
]);
// Iteration
console.log('\nEntries:');
for (const [key, value] of config) {
console.log(` ${key}: ${value}`);
}
console.log('Keys:', [...config.keys()]);
console.log('Values:', [...config.values()]);
Map accepts any value as a key, including objects, functions, and NaN. The constructor accepts an array of [key, value] pairs. Iteration follows insertion order, and destructuring works naturally in for...of loops.
// Performance comparison: frequent add/delete
const map = new Map();
const obj = {};
// Map has built-in size
map.set('a', 1).set('b', 2).set('c', 3);
console.log('Map size:', map.size);
console.log('Object size:', Object.keys(obj).length); // O(n)
// Map keys preserve type
const typedMap = new Map();
typedMap.set(1, 'number one');
typedMap.set('1', 'string one');
console.log('\nnumber 1:', typedMap.get(1));
console.log('string "1":', typedMap.get('1'));
// Object coerces keys to strings
const typedObj = {};
typedObj[1] = 'number one';
typedObj['1'] = 'string one'; // overwrites!
console.log('Object[1]:', typedObj[1]);
// Map as a counter
function wordCount(text) {
const counts = new Map();
const words = text.toLowerCase().split(/\s+/);
for (const word of words) {
counts.set(word, (counts.get(word) || 0) + 1);
}
return counts;
}
const counts = wordCount('the cat sat on the mat the cat');
console.log('\nWord counts:');
for (const [word, count] of counts) {
console.log(` ${word}: ${count}`);
}
Map.size is O(1) while Object.keys().length is O(n). Map preserves key types - 1 and '1' are different keys. Objects coerce all keys to strings, so numeric and string keys collide. The word counter shows Map's natural fit for counting/grouping.
// Creating a Set
const colors = new Set(['red', 'green', 'blue']);
colors.add('yellow');
colors.add('red'); // duplicate ignored
console.log('Size:', colors.size);
console.log('Has red:', colors.has('red'));
console.log('Has purple:', colors.has('purple'));
// Array deduplication
const numbers = [1, 3, 5, 3, 1, 7, 5, 9, 7];
const unique = [...new Set(numbers)];
console.log('\nOriginal:', numbers);
console.log('Unique:', unique);
// Object deduplication by property
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' },
{ id: 3, name: 'Charlie' },
{ id: 2, name: 'Bob' }
];
const seenIds = new Set();
const uniqueUsers = users.filter(user => {
if (seenIds.has(user.id)) return false;
seenIds.add(user.id);
return true;
});
console.log('\nUnique users:', uniqueUsers.map(u => u.name));
// Set operations (union, intersection, difference)
const setA = new Set([1, 2, 3, 4, 5]);
const setB = new Set([4, 5, 6, 7, 8]);
const union = new Set([...setA, ...setB]);
const intersection = new Set([...setA].filter(x => setB.has(x)));
const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log('\nA:', [...setA]);
console.log('B:', [...setB]);
console.log('Union:', [...union]);
console.log('Intersection:', [...intersection]);
console.log('Difference (A-B):', [...difference]);
Set automatically eliminates duplicates. The [...new Set(array)] pattern is the most concise way to deduplicate arrays. Set operations (union, intersection, difference) can be implemented using spread and filter with the has() method.
// Map <-> Array
const pairs = [['name', 'Alice'], ['age', 28], ['city', 'Portland']];
const map = new Map(pairs);
console.log('Map from array:', map);
const backToArray = [...map];
console.log('Map to array:', backToArray);
// Map <-> Object
const obj = { x: 1, y: 2, z: 3 };
const fromObj = new Map(Object.entries(obj));
console.log('\nMap from object:', fromObj);
const backToObj = Object.fromEntries(fromObj);
console.log('Map to object:', backToObj);
// Transform Map values
const prices = new Map([['apple', 1.5], ['banana', 0.75], ['cherry', 3.0]]);
const discounted = new Map(
[...prices].map(([fruit, price]) => [fruit, price * 0.9])
);
console.log('\nDiscounted:');
for (const [fruit, price] of discounted) {
console.log(` ${fruit}: ${price.toFixed(2)}`);
}
// Set <-> Array
const set = new Set([10, 20, 30, 40, 50]);
const doubled = [...set].map(n => n * 2);
console.log('\nDoubled set:', doubled);
const filtered = new Set([...set].filter(n => n > 25));
console.log('Filtered set:', [...filtered]);
// Chaining Set with Array methods
const tags = ['js', 'react', 'js', 'node', 'react', 'ts'];
const sortedUnique = [...new Set(tags)].sort();
console.log('Sorted unique tags:', sortedUnique);
Object.entries() and Object.fromEntries() bridge Map and Object. Spread converts Map/Set to arrays for using array methods like map, filter, sort. You can pass the result back to Map/Set constructors. This pattern lets you leverage the best of both worlds.
Map, Set, Map vs Object, Set deduplication, iteration, conversion