Metaprogramming in JavaScript: Proxies, Reflect & Strict Type Coercion
The Proxy API and the ToPrimitive coercion algorithm are the two most misunderstood engine-level primitives in JavaScript. Understanding them replaces cargo-culted defensive code with deliberate, predictable behavior — and reveals exactly how Vue 3 reactivity and React state comparison work under the hood.
Metaprogramming in JavaScript: Proxies, Reflect & Strict Type Coercion
The runtime is not a black box — but it is full of traps for engineers who haven't read the spec. Every == comparison runs a 12-step algorithm. Every arithmetic operation invokes ToPrimitive. Every property access on a Proxy triggers a C++-level trap handler. Most JavaScript engineers know these mechanisms exist; senior engineers know the exact steps, which is why they can predict the output of [] + {} or debug why Vue 3's reactivity stops working after a specific reassignment. This article reads those mechanisms from first principles.
1. The Proxy API
The Proxy object wraps a target and intercepts fundamental object operations through trap functions. The ECMAScript specification defines 13 interceptable operations; the three most consequential are get, set, and deleteProperty.
1.1 Intercepting Base Object Operations
1.2 Why Reflect.set Instead of Direct Assignment
This is the most common Proxy implementation mistake:
Always use Reflect.set(target, prop, value, receiver) inside set traps — not target[prop] = value. The receiver is the object that triggered the operation (which may be a child object using the proxy as its prototype). Direct assignment to target discards this distinction and creates subtle bugs that only manifest when Proxy is used in inheritance hierarchies.
2. Constructing Reactive State Managers with Proxy
Vue 3's reactive() is a thin wrapper around Proxy. Understanding the implementation removes the magic:
The Proxy get trap is how the reactive system discovers which state a computation depends on — by tracking every property read during the effect's execution. The set trap is how it notifies those computations of changes. The WeakMap keyed on the target ensures the tracking data is GC-eligible when the reactive object is discarded.
3. Building API SDKs with Proxy
4. Strict Coercion Rules
4.1 The ToPrimitive Algorithm
When JavaScript needs a primitive value from an object (for arithmetic, string concatenation, comparison), it calls the internal ToPrimitive(value, hint) operation. The hint can be "number", "string", or "default".
4.2 Truthy and Falsy Edge Cases
JavaScript has exactly 8 falsy values. Everything else is truthy — including [] and {}:
4.3 ==, ===, and Object.is()
![Comparison matrix diagram. Title: 'Equality Operator Reference'. Three columns: '==' (Abstract Equality), '===' (Strict Equality), 'Object.is()'. Six comparison rows: 1. 'NaN vs NaN' — false, false, TRUE (green highlight). 2. '+0 vs -0' — true, TRUE (cyan highlight), false (green highlight). 3. 'null vs undefined' — TRUE (cyan), false, false. 4. '"1" vs 1' — TRUE (cyan), false, false. 5. '[] vs false' — TRUE (cyan), false, false. 6. 'null vs null' — true, true, true. Bottom note in amber: 'Object.is() is the SameValue algorithm — the only equality that correctly handles NaN and -0. React, Redux, and Immer all use Object.is() for change detection.' Right column annotation for ===: 'Use === for most comparisons.' Right column annotation for Object.is(): 'Use Object.is() for NaN-safe change detection in state managers'.](https://pub-74778554195b4df89d82f0d61988355a.r2.dev/assets/blog/frontend/javascript-metaprogramming-proxy-reflect-coercion/fig-02.png)
Summary
| Concept | Rule |
|---|---|
| Proxy traps | Use Reflect.* to forward intercepted operations — preserves receiver and prototype chain |
set trap return value |
Return Reflect.set(target, prop, value, receiver) — not true — to correctly handle prototype scenarios |
| Vue 3 reactivity | get trap → track() records dependencies · set trap → trigger() notifies computed effects |
| ToPrimitive | hint: 'number' → valueOf() first · hint: 'string' → toString() first · override with Symbol.toPrimitive |
| Falsy values | Exactly 8: false, 0, -0, 0n, "", null, undefined, NaN — [] and {} are truthy |
NaN === NaN |
false — use Number.isNaN() or Object.is(NaN, NaN) (which is true) |
Object.is vs === |
Identical except: Object.is(NaN, NaN) = true · Object.is(+0, -0) = false |
| React state bail-out | React uses Object.is — setState(NaN) after useState(NaN) does NOT re-render |
What's Next
The series now moves to the runtime layer where JavaScript meets the operating system. Part 6 covers Node.js-specific I/O: EventEmitter internals, stream backpressure, and Worker Threads for CPU-bound work — the primitives that separate Node.js services that stay performant under load from those that quietly degrade. Part 6 → Node.js Streams, Event Emitters & Worker Threads
References
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.