duction-ready collection strategy requires aligning each primitive with its intended lifecycle and access pattern. Below is a step-by-step implementation demonstrating how to orchestrate all four primitives within a single request-tracking system.
Step 1: Initialize the Dynamic Registry with Map
Use Map when keys are non-string types, insertion order matters, or you require frequent additions/deletions. Unlike objects, Map preserves key types and provides native .size.
type RequestPayload = {
endpoint: string;
timestamp: number;
metadata: Record<string, unknown>;
};
class RequestRegistry {
private activeRequests: Map<string, RequestPayload> = new Map();
register(id: string, payload: RequestPayload): void {
this.activeRequests.set(id, payload);
}
retrieve(id: string): RequestPayload | undefined {
return this.activeRequests.get(id);
}
complete(id: string): boolean {
return this.activeRequests.delete(id);
}
get count(): number {
return this.activeRequests.size;
}
snapshot(): ReadonlyArray<[string, RequestPayload]> {
return Array.from(this.activeRequests.entries());
}
}
Architecture Rationale: Map is chosen here because request IDs may eventually shift from strings to UUID objects or numeric identifiers. The .size property avoids O(n) Object.keys().length calculations, and Array.from() provides a stable snapshot without mutating the underlying structure.
Step 2: Deduplicate and Validate with Set
Use Set when you only care about membership and uniqueness. It automatically enforces SameValueZero equality, making it ideal for tracking processed items or filtering duplicates.
class DeduplicationFilter {
private seenIdentifiers: Set<string> = new Set();
isDuplicate(identifier: string): boolean {
if (this.seenIdentifiers.has(identifier)) {
return true;
}
this.seenIdentifiers.add(identifier);
return false;
}
clear(): void {
this.seenIdentifiers.clear();
}
}
Architecture Rationale: Set eliminates the need for manual array filtering or object key coercion. The SameValueZero algorithm treats NaN === NaN and 0 === -0, which prevents edge-case duplicates that strict equality (===) would miss.
Use WeakMap when you need to associate data with objects without preventing garbage collection. Keys must be objects, and entries vanish automatically when the key is dereferenced.
type ElementMetrics = {
renderCount: number;
lastInteraction: number;
};
class DOMMetricStore {
private metrics: WeakMap<Element, ElementMetrics> = new WeakMap();
initialize(el: Element): void {
if (!this.metrics.has(el)) {
this.metrics.set(el, { renderCount: 0, lastInteraction: Date.now() });
}
}
incrementRender(el: Element): void {
const data = this.metrics.get(el);
if (data) {
data.renderCount++;
data.lastInteraction = Date.now();
}
}
getMetrics(el: Element): ElementMetrics | undefined {
return this.metrics.get(el);
}
}
Architecture Rationale: Attaching metrics directly to DOM nodes pollutes the element's property space and risks naming collisions. WeakMap keeps metadata external and garbage-collected. When the element is removed from the DOM and no other references exist, the metric object is automatically reclaimed, eliminating memory leaks in single-page applications.
Step 4: Track Object Lifecycle with WeakSet
Use WeakSet when you only need to mark objects as "processed", "active", or "branded" without storing associated values.
class ProcessTracker {
private completed: WeakSet<object> = new WeakSet();
markCompleted(item: object): void {
this.completed.add(item);
}
isCompleted(item: object): boolean {
return this.completed.has(item);
}
}
Architecture Rationale: WeakSet is strictly for membership testing. It cannot store values, iterate, or report size. This constraint is intentional: it enforces a lightweight, GC-safe flagging mechanism. Attempting to serialize or iterate a WeakSet will fail, which is a deliberate safeguard against accidental memory retention.
Pitfall Guide
1. Attempting to Use Primitives as WeakMap/WeakSet Keys
Explanation: WeakMap and WeakSet only accept objects as keys. Passing strings, numbers, or symbols throws a TypeError.
Fix: Always validate key types before insertion. If you need weak references for primitives, wrap them in an object or use a Map with explicit TTL expiration.
2. Expecting Deep Equality in Set Deduplication
Explanation: Set uses SameValueZero for equality checks. Two objects with identical properties are treated as distinct because they occupy different memory addresses.
Fix: Serialize objects to JSON strings before adding to a Set, or implement a custom deduplication function that compares structural properties.
3. Assuming WeakMap/WeakSet Support Iteration or Size
Explanation: These collections intentionally lack .size, .keys(), .values(), and for...of support. This prevents developers from accidentally retaining references during iteration.
Fix: If you need to audit or serialize weak collections, maintain a parallel strong-reference registry for debugging, or switch to Map/Set when lifecycle control is required.
4. Overusing WeakMap for Private Data When #privateFields Exist
Explanation: Modern JavaScript supports native private class fields (#field). WeakMap was historically used for encapsulation, but it adds indirection and GC overhead.
Fix: Use #privateFields for class-level encapsulation. Reserve WeakMap for external metadata attachment where you cannot modify the target object's prototype or class definition.
5. Ignoring Insertion Order Guarantees in Map/Set
Explanation: Map and Set preserve insertion order during iteration. Developers sometimes rely on this implicitly, then switch to Object or Array later, breaking deterministic traversal.
Fix: Explicitly document iteration expectations. If order is irrelevant, consider using a Map with sorted keys or a Set with explicit sorting before traversal.
6. Storing Mutable Objects as Map Keys Without Reference Awareness
Explanation: Map keys are compared by reference, not value. Mutating an object after using it as a key breaks lookup because the reference remains the same but the internal state changes.
Fix: Freeze objects used as keys (Object.freeze()), or derive keys from immutable identifiers (IDs, hashes) rather than mutable payloads.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Static configuration with known keys | Plain Object | Engine-optimized hidden classes, JSON serializable | Lowest runtime overhead |
| Dynamic key-value pairs with frequent updates | Map | O(1) lookups, preserves insertion order, native .size | Slightly higher memory than object, but faster mutations |
| Membership testing or deduplication | Set | Automatic uniqueness, SameValueZero equality, fast .has() | Minimal overhead, eliminates manual filtering |
| External metadata for DOM/external objects | WeakMap | GC-safe, prevents property pollution, auto-cleanup | Zero manual cleanup cost, prevents memory leaks |
| Object lifecycle flagging or type branding | WeakSet | Lightweight, GC-safe, enforces object-only keys | Lowest memory footprint for tracking |
Configuration Template
// collection-factory.ts
export type CollectionConfig = {
useMapForRegistry: boolean;
enableSetDeduplication: boolean;
attachWeakMetadata: boolean;
trackObjectLifecycle: boolean;
};
export function createCollectionSuite(config: CollectionConfig) {
const registry = config.useMapForRegistry ? new Map<string, unknown>() : null;
const dedupFilter = config.enableSetDeduplication ? new Set<string>() : null;
const metadataStore = config.attachWeakMetadata ? new WeakMap<object, unknown>() : null;
const lifecycleTracker = config.trackObjectLifecycle ? new WeakSet<object>() : null;
return {
registry,
dedupFilter,
metadataStore,
lifecycleTracker,
teardown(): void {
registry?.clear();
dedupFilter?.clear();
// WeakMap/WeakSet require no manual teardown
}
};
}
Quick Start Guide
- Identify the access pattern: Determine if you need key-value storage, uniqueness, external metadata, or lifecycle flagging.
- Select the primitive: Match the pattern to
Map, Set, WeakMap, or WeakSet using the decision matrix.
- Initialize with TypeScript: Declare explicit generic types to prevent runtime type coercion errors.
- Implement core operations: Use
.set(), .get(), .has(), .add(), .delete(), and .clear() as needed. Avoid manual iteration unless using Map/Set.
- Validate memory behavior: Run Chrome DevTools Memory tab or Node.js
--inspect to confirm weak collections release references when keys are dereferenced.