ict interfaces. This prevents the loose typing that often leads to Map/Set misuse.
interface RequestConfig {
endpoint: string;
method: 'GET' | 'POST';
payload?: Record<string, unknown>;
}
interface AuditEvent {
id: string;
type: string;
timestamp: number;
}
interface SessionState {
cacheRegistry: Map<RequestConfig, unknown>;
processedEvents: Set<string>;
eventLog: Map<string, AuditEvent>;
}
Step 2: Implement Dynamic Caching with Map
Use Map when keys are non-primitive or when insertion order matters. Here, we cache API responses using the request configuration object itself as the key.
class DataCache {
private registry: Map<RequestConfig, unknown>;
constructor() {
this.registry = new Map();
}
public retrieve(config: RequestConfig): unknown | undefined {
return this.registry.get(config);
}
public store(config: RequestConfig, data: unknown): void {
this.registry.set(config, data);
}
public getMetrics(): { totalEntries: number; keys: RequestConfig[] } {
return {
totalEntries: this.registry.size,
keys: [...this.registry.keys()]
};
}
}
Architecture Rationale: We chose Map over a plain object because RequestConfig is an object. Plain objects would coerce it to "[object Object]", causing cache collisions. Map preserves reference equality, allowing distinct configurations to coexist. The .size property provides O(1) metric tracking without iterating keys.
Step 3: Implement Event Deduplication with Set
Use Set when uniqueness and fast existence checks are the primary requirements.
class EventDeduplicator {
private fingerprintStore: Set<string>;
constructor(initialFingerprints?: string[]) {
this.fingerprintStore = new Set(initialFingerprints);
}
public hasProcessed(fingerprint: string): boolean {
return this.fingerprintStore.has(fingerprint);
}
public register(fingerprint: string): boolean {
const initialSize = this.fingerprintStore.size;
this.fingerprintStore.add(fingerprint);
// Returns true if the fingerprint was newly added, false if duplicate
return this.fingerprintStore.size > initialSize;
}
public getUniqueCount(): number {
return this.fingerprintStore.size;
}
}
Architecture Rationale: Set enforces uniqueness at the data structure level. The .has() method uses hash lookup, making it dramatically faster than Array.includes() for large datasets. The register method leverages the fact that Set.add() is idempotent, allowing us to detect duplicates by comparing size before and after insertion.
Step 4: Ordered Audit Logging with Map
When you need both key-based access and guaranteed insertion order, Map is the only native choice.
class AuditLogger {
private timeline: Map<string, AuditEvent>;
constructor() {
this.timeline = new Map();
}
public log(event: AuditEvent): void {
this.timeline.set(event.id, event);
}
public getChronologicalSnapshot(): AuditEvent[] {
// Map iteration guarantees insertion order
return [...this.timeline.values()];
}
public clearHistory(): void {
this.timeline.clear();
}
}
Architecture Rationale: Plain objects do not guarantee property enumeration order (especially with numeric-looking keys). Map strictly maintains insertion order, making it reliable for audit trails, replay systems, and UI rendering queues where sequence dictates state.
Pitfall Guide
1. Reference Equality Trap
Explanation: Map and Set use the SameValueZero algorithm (similar to ===). Two objects with identical properties are treated as different keys/values.
Fix: Extract a unique primitive identifier (UUID, string ID) before storing, or serialize complex keys if reference equality isn't required.
2. Set Value Deduplication Myth
Explanation: Developers assume Set deep-compares objects. It does not. new Set([{a:1}, {a:1}]) contains two items.
Fix: Store primitive identifiers in the Set, or use a Map<id, object> if you need to retrieve the full object later.
3. JSON Serialization Blind Spot
Explanation: JSON.stringify() converts Map and Set to {} and [] respectively, silently dropping all data.
Fix: Always convert to serializable formats before transmission: JSON.stringify([...map.entries()]) or JSON.stringify([...set]).
4. Index Access Expectation
Explanation: Set has no numeric indices. Attempting mySet[0] returns undefined.
Fix: Convert to an array when index-based access is required: const arr = [...mySet]; const first = arr[0];.
5. Memory Leak via Object Keys
Explanation: Map holds strong references to object keys. If the key object goes out of scope but remains in the Map, it prevents garbage collection.
Fix: Use WeakMap or WeakSet when keys are short-lived objects and you don't need to iterate or track size.
6. Over-Engineering Static Data
Explanation: Wrapping configuration objects or API responses in Map/Set adds unnecessary overhead and breaks destructuring.
Fix: Reserve Map/Set for dynamic, frequently mutated, or uniqueness-constrained data. Keep static payloads as plain objects/arrays.
7. Iteration Assumption
Explanation: While ECMAScript guarantees insertion order for Map and Set, some developers assume this order is stable across serialization/deserialization cycles.
Fix: Never rely on iteration order surviving JSON transport. Reconstruct order explicitly if needed.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Static configuration or API payload | Plain Object | Native JSON support, destructuring, V8 hidden class optimization | Lowest memory, fastest startup |
| Dynamic caching with complex keys | Map | Preserves object references, O(1) retrieval, no prototype pollution | Moderate memory, high lookup speed |
| Frequent add/remove operations | Map | Hash table structure optimized for mutations vs object hidden class rebuilds | Lower CPU overhead during mutations |
| Unique value tracking / deduplication | Set | Built-in uniqueness enforcement, O(1) existence checks | Eliminates O(n) scan overhead |
| Index-based access or transformations | Array | Native .map(), .filter(), .reduce(), numeric indexing | Required for ordered transformations |
| Short-lived object references | WeakMap / WeakSet | Allows garbage collection of keys, prevents memory leaks | Lower long-term memory footprint |
Configuration Template
// safe-serialization.ts
export const serializeMap = <K, V>(map: Map<K, V>): [K, V][] => {
return Array.from(map.entries());
};
export const deserializeMap = <K, V>(entries: [K, V][]): Map<K, V> => {
return new Map(entries);
};
export const serializeSet = <T>(set: Set<T>): T[] => {
return Array.from(set);
};
export const deserializeSet = <T>(items: T[]): Set<T> => {
return new Set(items);
};
// lru-cache.ts (Production-ready Map extension)
export class LRUCache<K, V> {
private cache: Map<K, V>;
private capacity: number;
constructor(capacity: number) {
this.cache = new Map();
this.capacity = capacity;
}
get(key: K): V | undefined {
if (!this.cache.has(key)) return undefined;
const value = this.cache.get(key)!;
// Refresh position: delete and re-insert to maintain insertion order
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
set(key: K, value: V): void {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
// Evict oldest entry (first key in insertion order)
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, value);
}
clear(): void {
this.cache.clear();
}
}
Quick Start Guide
- Identify the bottleneck: Locate arrays using
.includes() for existence checks or objects with dynamic key addition. Replace them with Set or Map respectively.
- Initialize with data: Pass iterables directly to constructors:
new Set(existingArray) or new Map(objectEntries). This avoids manual .add()/.set() loops.
- Implement serialization guards: Wrap all
Map/Set instances in the provided serialization utilities before sending data over HTTP or storing in localStorage.
- Add eviction logic: For caches or session stores, wrap
Map in an LRU pattern or implement a time-based cleanup routine to prevent unbounded memory growth.
- Validate with TypeScript: Apply strict generic types (
Map<string, User>, Set<number>) to catch key/value mismatches at compile time rather than runtime.