undle size (no third-party utilities needed), and aligns runtime behavior with modern engine optimizations.
Core Solution
Implementing these primitives correctly requires understanding their memory semantics and access patterns. Below is a production-grade architecture that demonstrates how to orchestrate all four collections in a cohesive system. This example builds a DocumentRegistry that manages active resources, validates file extensions, attaches lifecycle metadata to DOM nodes, and tracks processed mutation records.
Architecture Decisions & Rationale
Map for Active Resources: We need deterministic insertion order, frequent additions/deletions, and string-based keys. Map guarantees order preservation and avoids prototype pollution. The .size property provides O(1) count retrieval without manual tracking.
Set for Extension Validation: File extensions require fast membership testing. Set provides O(1) .has() lookups and automatically rejects duplicates during initialization.
WeakMap for DOM Metadata: Attaching render counts and dirty flags to HTMLElement instances requires weak references. If we used a Map, removed DOM nodes would remain in memory because the map holds strong references. WeakMap keys are garbage-collected when the element is detached, preventing leaks.
WeakSet for Mutation Tracking: MutationRecord objects are transient. We only need to know if a specific record has already been processed. WeakSet provides O(1) membership checks without retaining references to discarded records.
Implementation
interface ResourceMeta {
lastAccessed: number;
priority: 'low' | 'medium' | 'high';
}
interface ElementState {
renderCount: number;
isDirty: boolean;
}
class DocumentRegistry {
private activeResources = new Map<string, ResourceMeta>();
private allowedExtensions = new Set<string>(['.pdf', '.docx', '.txt', '.md']);
private elementMetadata = new WeakMap<HTMLElement, ElementState>();
private processedMutations = new WeakSet<MutationRecord>();
registerResource(id: string, meta: ResourceMeta): void {
this.activeResources.set(id, meta);
}
isExtensionAllowed(extension: string): boolean {
return this.allowedExtensions.has(extension);
}
attachElementState(el: HTMLElement, state: ElementState): void {
this.elementMetadata.set(el, state);
}
getElementState(el: HTMLElement): ElementState | undefined {
return this.elementMetadata.get(el);
}
markMutationProcessed(record: MutationRecord): void {
this.processedMutations.add(record);
}
hasProcessedMutation(record: MutationRecord): boolean {
return this.processedMutations.has(record);
}
getActiveResourceCount(): number {
return this.activeResources.size;
}
evictStaleResources(maxAgeMs: number): void {
const cutoff = Date.now() - maxAgeMs;
for (const [id, meta] of this.activeResources) {
if (meta.lastAccessed < cutoff) {
this.activeResources.delete(id);
}
}
}
clearAllResources(): void {
this.activeResources.clear();
}
}
Why This Structure Works
- Type Safety: TypeScript interfaces enforce shape contracts for metadata and state, preventing runtime type mismatches.
- Encapsulation: Private fields prevent external mutation of collection internals. Access is mediated through explicit methods.
- Memory Discipline:
WeakMap and WeakSet are used exclusively for object references that have independent lifecycles. Map and Set manage data with explicit lifecycle control.
- Performance Alignment: Lookup operations use
.has() and .get() instead of .find() or Object.keys(), leveraging engine-optimized hash tables.
Pitfall Guide
1. Assuming WeakMap/WeakSet Are Iterable
Explanation: Developers frequently attempt to loop over WeakMap or WeakSet entries using for...of or .forEach(). These structures intentionally lack iteration methods because weak references can be garbage-collected at any time, making enumeration non-deterministic.
Fix: Use Map or Set if you need to enumerate entries. Reserve WeakMap/WeakSet strictly for metadata attachment or membership tracking where iteration is unnecessary.
2. Using Primitives as WeakMap Keys
Explanation: WeakMap only accepts objects (or non-registered symbols) as keys. Passing a string, number, or boolean throws a TypeError. This is a common mistake when migrating from Map or Object.
Fix: Validate key types before insertion. If you need primitive keys with weak semantics, wrap them in a lightweight object or use a Map with manual cleanup logic.
3. Confusing Reference Equality with Value Equality
Explanation: Map and Set use the SameValueZero algorithm for key comparison. Two distinct objects with identical properties are treated as different keys. Developers often expect deep equality, leading to missed lookups.
Fix: Serialize objects to strings if you need value-based keys, or use a dedicated hashing utility. For object keys, ensure you pass the exact same reference used during insertion.
4. Over-Engineering Small Collections
Explanation: Set and Map carry initialization overhead. For arrays with fewer than 50 items, Array.includes() or Object lookups often outperform collection primitives due to JIT optimization and lower memory allocation.
Fix: Profile before switching. Use Set/Map when collections exceed 100 items, require frequent mutations, or demand O(1) membership testing. Keep small datasets in arrays/objects for simplicity.
5. Ignoring NaN Behavior in Map/Set
Explanation: NaN === NaN evaluates to false in JavaScript, but Map and Set treat NaN as a valid, unique key. Developers expecting strict equality semantics may be surprised when map.has(NaN) returns true after a single insertion.
Fix: Leverage this behavior intentionally for caching or deduplication. Document the expectation in code comments to prevent confusion during code reviews.
6. Mutating Objects Used as WeakMap Keys
Explanation: WeakMap tracks keys by reference, not by content. If you mutate an object after using it as a key, the reference remains valid, but the mutated state may break business logic expectations. Conversely, replacing the object reference breaks the weak link.
Fix: Treat WeakMap keys as immutable identifiers. If state changes, update the value stored in the map, not the key itself. Freeze objects used as keys if mutation is a risk.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Frequent key-value lookups with string/symbol keys | Map | O(1) lookup, no prototype overhead, preserves insertion order | Low memory, high CPU efficiency |
| Membership testing on large datasets (>1k items) | Set | Hash-based O(1) .has() vs O(n) array scan | Minimal memory, massive CPU savings |
| Attaching metadata to DOM nodes or transient objects | WeakMap | Automatic GC cleanup prevents memory leaks | Near-zero memory overhead, zero manual cleanup |
| Tracking processed events or preventing duplicate object handling | WeakSet | O(1) membership check without retaining references | Low memory, eliminates stale state bugs |
| Static configuration or JSON serialization | Object | Native JSON compatibility, familiar syntax | Lowest overhead, but lacks iteration guarantees |
Configuration Template
// collection-utils.ts
// Production-ready wrapper with type safety and common operations
export class TypedMap<K, V> {
private store = new Map<K, V>();
set(key: K, value: V): this {
this.store.set(key, value);
return this;
}
get(key: K): V | undefined {
return this.store.get(key);
}
has(key: K): boolean {
return this.store.has(key);
}
delete(key: K): boolean {
return this.store.delete(key);
}
get size(): number {
return this.store.size;
}
clear(): void {
this.store.clear();
}
entries(): IterableIterator<[K, V]> {
return this.store.entries();
}
keys(): IterableIterator<K> {
return this.store.keys();
}
values(): IterableIterator<V> {
return this.store.values();
}
}
export class TypedSet<T> {
private store = new Set<T>();
add(value: T): this {
this.store.add(value);
return this;
}
has(value: T): boolean {
return this.store.has(value);
}
delete(value: T): boolean {
return this.store.delete(value);
}
get size(): number {
return this.store.size;
}
clear(): void {
this.store.clear();
}
toArray(): T[] {
return Array.from(this.store);
}
union(other: TypedSet<T>): TypedSet<T> {
const result = new TypedSet<T>();
for (const item of this.store) result.add(item);
for (const item of other.store) result.add(item);
return result;
}
intersection(other: TypedSet<T>): TypedSet<T> {
const result = new TypedSet<T>();
for (const item of this.store) {
if (other.has(item)) result.add(item);
}
return result;
}
}
Quick Start Guide
- Initialize Collections: Replace inline object/array patterns with
new Map(), new Set(), new WeakMap(), or new WeakSet() based on your access pattern.
- Define Key Contracts: Ensure
WeakMap/WeakSet only receive object references. Use strings/numbers/symbols for Map/Set.
- Replace Linear Scans: Swap
Array.includes() or Object.values().find() with Set.has() or Map.get() for immediate performance gains.
- Attach Metadata Safely: Use
WeakMap to bind state to DOM elements or third-party objects without manual cleanup routines.
- Validate in CI: Add linting rules to flag
Array.includes() on large datasets and enforce explicit collection type annotations in TypeScript projects.