data structure guarantees with application requirements. Below is a production-grade implementation strategy using TypeScript, demonstrating how to leverage each type's strengths while avoiding common architectural traps.
1. Dynamic Key-Value Registry (Map)
Use Map when keys are not guaranteed to be strings, when insertion order matters, or when frequent additions/deletions occur.
type CacheEntry<T> = {
payload: T;
timestamp: number;
ttl: number;
};
class DynamicCache<T> {
private store: Map<string | number | symbol, CacheEntry<T>>;
constructor() {
this.store = new Map();
}
set(key: string | number | symbol, value: T, ttlMs: number = 300_000): void {
this.store.set(key, {
payload: value,
timestamp: performance.now(),
ttl: ttlMs,
});
}
get(key: string | number | symbol): T | undefined {
const entry = this.store.get(key);
if (!entry) return undefined;
const isExpired = performance.now() - entry.timestamp > entry.ttl;
if (isExpired) {
this.store.delete(key);
return undefined;
}
return entry.payload;
}
get activeCount(): number {
return this.store.size;
}
}
Architecture Rationale: Map provides .size for O(1) cardinality checks, guarantees insertion order for predictable serialization, and accepts symbol keys for private namespace isolation. The TTL eviction pattern leverages performance.now() for high-resolution timing without blocking the main thread.
Use Set when uniqueness constraints and membership testing are primary concerns.
interface EventPayload {
id: string;
type: string;
metadata: Record<string, unknown>;
}
class EventDeduplicator {
private seenIds: Set<string>;
private processed: Set<string>;
constructor() {
this.seenIds = new Set();
this.processed = new Set();
}
ingest(event: EventPayload): boolean {
if (this.seenIds.has(event.id)) {
return false;
}
this.seenIds.add(event.id);
return true;
}
markComplete(eventId: string): void {
this.processed.add(eventId);
}
getPendingCount(): number {
return this.seenIds.size - this.processed.size;
}
clearCompleted(): void {
for (const id of this.processed) {
this.seenIds.delete(id);
}
this.processed.clear();
}
}
Architecture Rationale: Set uses the SameValueZero algorithm for equality checks, which correctly handles NaN and distinguishes between 0 and -0. The dual-set pattern separates ingestion tracking from completion state, enabling O(1) lookups during high-throughput event processing.
Use WeakMap when attaching state to objects that may be destroyed independently of your application logic.
interface InteractionState {
clickCount: number;
lastInteraction: number;
hoverDuration: number;
}
class DOMInteractionTracker {
private metadata: WeakMap<Element, InteractionState>;
constructor() {
this.metadata = new WeakMap();
}
initialize(element: Element): void {
if (!this.metadata.has(element)) {
this.metadata.set(element, {
clickCount: 0,
lastInteraction: 0,
hoverDuration: 0,
});
}
}
recordClick(element: Element): void {
const state = this.metadata.get(element);
if (state) {
state.clickCount++;
state.lastInteraction = performance.now();
}
}
getState(element: Element): InteractionState | undefined {
return this.metadata.get(element);
}
}
Architecture Rationale: WeakMap keys are held weakly. When an Element is removed from the DOM and loses all external references, the V8 garbage collector automatically reclaims both the element and its associated WeakMap entry. This eliminates manual removeEventListener cleanup routines and prevents the most common source of SPA memory leaks.
4. Object Tagging & Validation (WeakSet)
Use WeakSet for lightweight marking, validation, or tracking object lifecycle states without storing associated data.
class ComponentValidator {
private initialized: WeakSet<object>;
private disposed: WeakSet<object>;
constructor() {
this.initialized = new WeakSet();
this.disposed = new WeakSet();
}
markInitialized(instance: object): void {
this.initialized.add(instance);
}
markDisposed(instance: object): void {
this.disposed.add(instance);
}
assertActive(instance: object): void {
if (!this.initialized.has(instance)) {
throw new Error('Component not initialized');
}
if (this.disposed.has(instance)) {
throw new Error('Component already disposed');
}
}
}
Architecture Rationale: WeakSet stores only object references and provides O(1) membership checks. Because it cannot be iterated and holds no size property, it enforces a strict tagging pattern that cannot accidentally leak memory or expose internal state.
Pitfall Guide
1. Passing Primitives to WeakMap/WeakSet
Explanation: WeakMap and WeakSet only accept objects as keys. Passing strings, numbers, booleans, or null throws a TypeError at runtime.
Fix: Validate key types before insertion. Use typeof key === 'object' && key !== null as a guard, or fall back to Map/Set when primitive keys are required.
2. Assuming Set Deduplicates Objects by Value
Explanation: Set uses reference equality for objects. Two objects with identical properties are treated as distinct entries if they occupy different memory addresses.
Fix: Serialize objects to strings or extract a unique identifier before insertion. Alternatively, use a Map keyed by a computed hash or ID field.
3. Attempting to Iterate WeakMap/WeakSet
Explanation: These structures intentionally lack .keys(), .values(), .entries(), and .size. Iteration would require keeping keys alive, defeating the purpose of weak references.
Fix: If you need enumeration, maintain a parallel Set of keys or switch to Map/Set with explicit cleanup routines. Accept that weak collections are strictly for association, not traversal.
4. Using Map for Static Configuration
Explanation: Map instances cannot be serialized with JSON.stringify() without custom replacers. They also carry higher memory overhead than plain objects for static, string-keyed data.
Fix: Reserve Map for dynamic, runtime-generated key-value pairs. Use {} or Record<string, T> for configuration objects, API responses, and data that requires JSON serialization.
5. Ignoring Map's Insertion Order Guarantee
Explanation: Map preserves the order in which entries were added. Relying on this behavior for business logic can cause subtle bugs when entries are deleted and re-added, as re-inserted keys move to the end of the iteration sequence.
Fix: Do not use Map as a sorted data structure. If order matters, maintain a separate sorted array of keys or use a dedicated data structure like a binary heap or balanced tree.
6. Memory Leaks from Regular Map Caches
Explanation: Storing large payloads or DOM references in a standard Map creates strong references that prevent garbage collection. Over time, this causes heap growth and increased GC pause times.
Fix: Profile memory usage with Chrome DevTools or Node.js --inspect. Replace Map with WeakMap when keys are objects with independent lifecycles. Implement LRU eviction policies for primitive-keyed caches.
7. Over-Engineering Simple Lookups
Explanation: Wrapping boolean flags or simple existence checks in Set or Map adds unnecessary abstraction layers and memory overhead.
Fix: Use plain object properties, boolean flags, or Map/Set only when you need O(1) membership testing across dynamic datasets. Keep simple state simple.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Static configuration with string keys | Object {} | JSON-serializable, lower memory overhead, familiar syntax | Low |
| Dynamic keys (numbers, symbols, objects) | Map | No key coercion, guaranteed insertion order, O(1) operations | Medium |
| High-frequency membership testing | Set | O(1) lookup vs O(n) array scan, built-in uniqueness | Medium |
| DOM node metadata attachment | WeakMap | Automatic garbage collection, zero manual cleanup | Low |
| Object lifecycle tagging/validation | WeakSet | Lightweight reference tracking, no data storage overhead | Low |
| Cache with primitive keys & TTL | Map + eviction logic | Strong references required for primitives, predictable lifecycle | Medium |
| Large dataset deduplication | Set | Native uniqueness enforcement, O(1) insertion/lookup | Medium |
Configuration Template
// collection-registry.ts
// Production-ready collection factory with type safety and lifecycle hooks
type CollectionType = 'map' | 'set' | 'weakmap' | 'weakset';
interface CollectionConfig<T> {
type: CollectionType;
ttlMs?: number;
maxSize?: number;
onEvict?: (key: unknown, value: T) => void;
}
export class CollectionFactory {
static create<T>(config: CollectionConfig<T>): Map<unknown, T> | Set<T> | WeakMap<object, T> | WeakSet<object> {
switch (config.type) {
case 'map':
return new Map<unknown, T>();
case 'set':
return new Set<T>();
case 'weakmap':
return new WeakMap<object, T>();
case 'weakset':
return new WeakSet<object>();
default:
throw new Error(`Unsupported collection type: ${config.type}`);
}
}
static withTTL<T>(store: Map<string | number, T>, ttlMs: number, onEvict?: (key: string | number, value: T) => void): Map<string | number, T> {
const wrapped = new Map<string | number, { value: T; expires: number }>();
return new Proxy(wrapped, {
get(target, prop) {
if (prop === 'get') {
return (key: string | number) => {
const entry = target.get(key);
if (!entry) return undefined;
if (performance.now() > entry.expires) {
target.delete(key);
onEvict?.(key, entry.value);
return undefined;
}
return entry.value;
};
}
if (prop === 'set') {
return (key: string | number, value: T) => {
target.set(key, { value, expires: performance.now() + ttlMs });
return wrapped;
};
}
return Reflect.get(target, prop);
}
}) as unknown as Map<string | number, T>;
}
}
Quick Start Guide
- Identify Collection Boundaries: Locate arrays used for membership testing and objects used as dynamic dictionaries. Replace them with
Set and Map respectively.
- Audit DOM References: Search for
Map instances storing Element or Node objects. Migrate them to WeakMap to enable automatic garbage collection.
- Enforce Type Constraints: Add runtime type guards for
WeakMap/WeakSet keys. Use TypeScript generics to prevent primitive key insertion at compile time.
- Profile Memory Footprint: Run Chrome DevTools Memory panel or Node.js
--heapsnapshot-signal=SIGUSR2. Compare heap snapshots before and after migration to verify reduced GC pressure.
- Implement Eviction Policies: For
Map-based caches, add TTL expiration or LRU eviction logic to prevent unbounded growth. Test under sustained load to validate memory stability.