Data Structures in JavaScript: When to Use What (2026)
Arrays aren't the only tool. Pick the right data structure and your code gets faster, cleaner, and more readable.
The Big Picture
Most developers use Arrays for everything.
But JavaScript has more options:
Array → Ordered list, fast access by index
Set → Unique values only, fast lookup
Map → Key-value pairs (any type as key)
Object → Key-value (string keys only), structured data
WeakMap → Key-value with garbage-collectable keys
WeakSet → Set with GC-friendly references
The right choice can turn O(n) into O(1).
Array — Your Default Choice
// Best for: Ordered collections, sequences, stacks, queues
const items = ['apple', 'banana', 'cherry'];
// Core operations:
items.push('date'); // Add to end: O(1)
items.pop(); // Remove from end: O(1)
items.shift(); // Remove from start: O(n) — reindexes!
items.unshift('a'); // Add to start: O(n) — reindexes!
items[0]; // Access by index: O(1)
items.includes('banana'); // Search: O(n)
items.indexOf('banana'); // Find index: O(n)
items.find(x => x > 'b'); // Find by condition: O(n)
items.filter(x => x.length > 5); // Filter: O(n)
items.map(x => x.toUpperCase()); // Transform: O(n)
items.reduce((a, b) => a + b); // Aggregate: O(n)
// When to use Array:
// → Order matters (list of tasks, timeline)
// → Need indexed access
// → May contain duplicates
// → Need to iterate in sequence
// → Stack/queue operations (push/pop or push/shift)
// Performance tips:
// Use .at() for negative indexing
items.at(-1); // Last element
items.at(-2); // Second to last
// Use .with() for immutable updates (Node.js 20+)
const updated = items.with(1, 'blueberry'); // New array, index 1 changed
Set — Unique Values Only
// Best for: Deduplication, membership testing, set operations
const ids = new Set([1, 2, 3, 2, 1, 4]); // {1, 2, 3, 4} — duplicates removed!
// Core operations:
ids.add(5); // Add: O(1) average
ids.has(3); // Check existence: O(1) average ⚡
ids.delete(2); // Remove: O(1) average
ids.size; // Count: O(1)
ids.forEach(id => console.log(id)); // Iterate: O(n)
[...ids]; // Convert to array: O(n)
// Practical examples:
// 1. Deduplicate an array
const dupes = [1, 2, 2, 3, 3, 3, 4];
const unique = [...new Set(dupes)]; // [1, 2, 3, 4]
// 2. Fast membership test (vs Array.includes is O(n))
const userRoles = new Set(['admin', 'editor']);
if (userRoles.has('admin')) { /* ✅ O(1)! */ }
// vs: if (userRoles.includes('admin')) /* ❌ O(n) */
// 3. Set operations
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
// Union (all items from both)
const union = new Set([...setA, ...setB]); // {1,2,3,4,5,6}
// Intersection (common items)
const intersection = new Set([...setA].filter(x => setB.has(x))); // {3,4}
// Difference (in A but not B)
const difference = new Set([...setA].filter(x => !setB.has(x))); // {1,2}
// 4. Remove duplicates from array of objects
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' }, // Duplicate!
];
const uniqueUsers = [...new Map(users.map(u => [u.id, u])).values()];
// Uses id as key, automatically deduplicates!
// When to use Set:
// → Values must be unique
// → Frequent "does this exist?" checks
// → Need to remove duplicates
// → Set math (union, intersection, difference)
Map — Flexible Key-Value Store
// Best for: Any-type keys, frequent add/remove, ordered iteration
const cache = new Map();
// Core operations:
cache.set('key1', { data: 'value' }); // Set: O(1)
cache.set(123, 'number key'); // Number as key! Object can't do this.
cache.set({ id: 1 }, 'object key'); // Object as key!
cache.set(myElement, 'DOM key'); // DOM element as key!
cache.get('key1'); // Get: O(1) average
cache.has('key1'); // Check: O(1)
cache.delete('key1'); // Remove: O(1)
cache.size; // Count: O(1)
// Iteration preserves insertion order
for (const [key, value] of cache) {
console.log(key, value);
}
// Why Map > Object for many cases:
// 1. Any type of key (Objects only allow strings/Symbols)
const objKey = { id: 42 };
myMap.set(objKey, 'found'); // Works!
// myObj[objKey] = 'found'; // Becomes "[object Object]" key!
// 2. Size property (no need to Object.keys().length)
console.log(map.size); // O(1)
console.log(Object.keys(obj).length); // O(n) — creates array first!
// 3. Performance at scale
// Map.set/get/has are slightly faster than Object for frequent adds/deletes
// Especially true when keys are added/removed frequently
// 4. Iterable (no need for Object.entries())
for (const [k, v] of map) { } // Direct iteration
for (const [k, v] of Object.entries(obj)) { } // Creates intermediate array
// 5. No prototype pollution risk
// Objects inherit from Object.prototype (toString, hasOwnProperty, etc.)
// Maps are clean — no inherited keys
// Practical example: LRU Cache
class LRUCache {
#map = new Map();
#maxSize;
constructor(maxSize = 100) {
this.#maxSize = maxSize;
}
get(key) {
if (!this.#map.has(key)) return undefined;
const value = this.#map.get(key);
// Re-insert to mark as most recently used (Map preserves insertion order!)
this.#map.delete(key);
this.#map.set(key, value);
return value;
}
set(key, value) {
if (this.#map.has(key)) this.#map.delete(key);
if (this.#map.size >= this.#maxSize) {
// Delete oldest (first inserted = least recently used)
const oldestKey = this.#map.keys().next().value;
this.#map.delete(oldestKey);
}
this.#map.set(key, value);
}
}
// When to use Map:
// → Keys that aren't strings (numbers, objects, functions)
// → Frequent additions and deletions
// → Need to know size efficiently
// → Order of insertion matters
// → Building caches, dictionaries, lookups
Object — Structured Data
// Best for: Fixed schemas, JSON serialization, configuration
const user = {
id: 'usr_123',
email: 'alice@example.com',
name: 'Alice',
role: 'admin', // String key only!
};
// Core operations:
user.name; // Access: O(1)
user.name = 'Alice Smith'; // Set: O(1)
'user' in user; // Has check: O(1)
delete user.role; // Remove: O(1) (but can affect V8 optimization!)
// Destructuring (super useful!)
const { name, email, ...rest } = user;
// Spread & merge
const updatedUser = { ...user, name: 'Alice Smith', lastLogin: Date.now() };
// When to use Object:
// → Fixed schema/shape (like a database record)
// → Need JSON.stringify() compatibility
// → Configuration objects
// → Passing named parameters to functions
// → Simple key-value where keys are known strings
// DON'T use Object when:
// → You need non-string keys → Use Map
// → Frequent key addition/removal → Use Map
// → You need size tracking → Use Map
// → Order of keys matters → Use Map (preserves insertion order since ES2015)
WeakMap & WeakSet — Memory-Safe References
// WeakMap: Keys MUST be objects, values can be anything
// Keys are garbage-collected when no other reference exists!
const metadata = new WeakMap();
function attachMeta(element, data) {
metadata.set(element, data); // element is the key
}
let div = document.createElement('div');
attachMeta(div, { created: Date.now(), clicks: 0 });
// Later...
div = null; // No more references to div → GC collects both div AND the metadata entry!
// With regular Map, this would be a memory leak.
// Real-world use cases:
// 1. Private class fields (before #private syntax)
class Counter {
#counts = new WeakMap();
getCount(instance) {
return this.#counts.get(instance) || 0;
}
increment(instance) {
this.#counts.set(instance, this.getCount(instance) + 1);
}
}
// 2. DOM element metadata (without polluting element properties)
const elementData = new WeakMap();
function setData(el, key, value) {
let map = elementData.get(el);
if (!map) { map = new Map(); elementData.set(el, map); }
map.set(key, value);
}
// 3. Caching computed values for objects
const cache = new WeakMap();
function computeHeavy(obj) {
if (cache.has(obj)) return cache.get(obj);
const result = /* expensive computation */ obj.data * 2;
cache.set(obj, result);
return result;
}
// When obj is GC'd, cache entry auto-removed → no memory leak!
// WeakSet: Like Set but only holds object references (weakly)
const processed = new WeakSet();
function processOnce(item) {
if (processed.has(item)) return; // Already done
doWork(item);
processed.add(item); // Mark as processed
}
// When item is GC'd, entry auto-removed
// When to use WeakMap/WeakSet:
// → Storing metadata on objects without preventing GC
// → Caching computed values for objects
// → Tracking state without memory leaks
// → Private data storage
// ⚠️ Limitation: Not iterable (can't list all entries — by design!)
Performance Comparison
// Setup: 100,000 items
const n = 100_000;
const arr = Array.from({ length: n }, (_, i) => i);
const set = new Set(arr);
const map = new Map(arr.map(i => [i, `value_${i}`]));
const obj = Object.fromEntries(arr.map(i => [i, `value_${i}`]));
// Lookup speed comparison:
// Array.includes() — O(n): ~5ms for 100K items
// Set.has() — O(1): ~0.005ms for 100K items (1000x faster!)
// Map.get() — O(1): ~0.007ms
// Object property — O(1): ~0.001ms (slightly faster than Map for string keys)
// But for string-only keys, Object is often marginally faster than Map
// The real win for Map is flexibility + safety + semantics
// Insertion/deletion at scale:
// Array.push() — O(1) amortized
// Array.splice(0,1) — O(n) — AVOID for large arrays!
// Set.add/delete — O(1) average
// Map.set/delete — O(1) average
// Object delete — Can deoptimize V8 hidden class
Decision Flowchart
Need to store data?
│
├─ Keys are strings, fixed schema? → Object
│ (configuration, records, JSON-bound data)
│
├─ Need unique values only? → Set
│ (dedup, membership tests, set operations)
│
├─ Non-string keys OR frequent add/remove? → Map
│ (caches, dictionaries, flexible lookups)
│
├─ Need ordered collection with index access? → Array
│ (lists, sequences, stacks, queues)
│
├─ Metadata on objects, don't want memory leaks? → WeakMap
│ (DOM metadata, private data, object caching)
│
└─ Track objects without preventing GC? → WeakSet
(processed flags, instance tracking)
Quick Reference
| Type | Key Type | Iterable | Size Prop | GC Safe | JSON |
|---|---|---|---|---|---|
| Array | number (index) | ✅ | .length |
N/A | ✅ |
| Set | any | ✅ | .size |
N/A | ❌* |
| Map | any | ✅ | .size |
N/A | ❌* |
| Object | string/Symbol | ❌** | manual | N/A | ✅ |
| WeakMap | object only | ❌ | N/A | ✅ | ❌ |
| WeakSet | object only | ❌ | N/A | ✅ | ❌ |
*Can convert via JSON.stringify([...map]) or spread operator
**Use Object.keys/values/entries() for iteration
Which data structure do you overuse? Which one should you be using more?
Follow @armorbreak for more practical JS guides.
Top comments (0)