Featured image of post JavaScript Proxy and Reflect API: Metaprogramming Patterns Featured image of post JavaScript Proxy and Reflect API: Metaprogramming Patterns

JavaScript Proxy and Reflect API: Metaprogramming Patterns

Explore JavaScript Proxy and Reflect API for metaprogramming. Learn 13 traps, Reflect methods, validation, virtual properties, and real-world patterns in Vue 3 and MobX.

JavaScript’s Proxy and Reflect APIs are among the most powerful metaprogramming tools available in the language. They allow developers to intercept and customize fundamental operations on objects — property access, assignment, enumeration, function invocation, and even constructor calls. This article explores all 13 proxy traps, the complementary Reflect API, and real-world patterns used in production frameworks like Vue 3, MobX, and Immer.

Understanding the Proxy Pattern

A Proxy wraps a target object and intercepts operations via handler functions called traps:

const target = { name: "Alice", age: 30 };
const handler = {
  get(target, prop, receiver) {
    console.log(`Getting property "${prop}"`);
    return Reflect.get(target, prop, receiver);
  }
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // Logs: Getting property "name" → "Alice"

The target remains unmodified. The handler defines how operations on the proxy are forwarded, modified, or blocked. Proxy differs from wrapper functions or decorators because it intercepts at the language level — in, delete, for...in, and new can all be customized.


The 13 Proxy Traps

Proxy provides 13 traps that cover every fundamental object operation. The most commonly used are get and set, which intercept property reading and writing. Here is a complete reference:

TrapFires WhenArguments
getProperty readtarget, prop, receiver
setProperty writetarget, prop, value, receiver
hasin operatortarget, prop
deletePropertydelete operatortarget, prop
ownKeysObject.keys(), for...intarget
applyFunction calltarget, thisArg, args
constructnew keywordtarget, args, newTarget
getPrototypeOfObject.getPrototypeOftarget
setPrototypeOfObject.setPrototypeOftarget, proto
isExtensibleObject.isExtensibletarget
preventExtensionsObject.preventExtensionstarget
getOwnPropertyDescriptorObject.getOwnPropertyDescriptortarget, prop
definePropertyObject.definePropertytarget, prop, desc

Each trap must enforce invariants. For example, a get trap cannot return a value different from the actual value for a non-writable, non-configurable property.


The Reflect API

Reflect methods mirror every proxy trap and provide the default forwarding behavior. Using Reflect inside traps ensures correct propagation of the receiver argument, which is critical for proper this binding in inherited properties and getters:

const parent = {
  get fullName() { return `${this.first} ${this.last}`; }
};
const handler = {
  get(target, prop, receiver) {
    // Without Reflect.get, the getter's 'this' would be wrong
    return Reflect.get(target, prop, receiver);
  }
};
const proxy = new Proxy(Object.create(parent), handler);
proxy.first = "John";
proxy.last = "Doe";
console.log(proxy.fullName); // "John Doe"

The mental model is simple: Reflect is to Proxy what Object is to direct property access. Always forward traps through Reflect to maintain default behavior while adding custom logic.


Validation and Data Integrity

The set trap can enforce schema constraints on objects:

const validator = {
  set(target, prop, value) {
    if (prop === "age" && (!Number.isInteger(value) || value < 0)) {
      throw new TypeError("Age must be a positive integer");
    }
    if (prop === "email" && !/^[^\s@]+@[^\s@]+$/.test(value)) {
      throw new TypeError("Invalid email format");
    }
    return Reflect.set(target, prop, value);
  }
};
const user = new Proxy({}, validator);
user.age = 25;  // OK
user.age = -5;  // TypeError: Age must be a positive integer

A read-only proxy blocks all mutation traps:

const readOnly = target =>
  new Proxy(target, {
    set: () => { throw new Error("Object is read-only"); },
    deleteProperty: () => { throw new Error("Object is read-only"); },
    defineProperty: () => { throw new Error("Object is read-only"); },
  });

Virtual Properties and Computed Values

Proxy can expose properties that do not exist on the target object by computing them dynamically in the get trap:

const range = (from, to) =>
  new Proxy({ from, to }, {
    get(target, prop) {
      if (prop === "size") return target.to - target.from + 1;
      if (prop === "contains") return (v) => v >= target.from && v <= target.to;
      return Reflect.get(target, prop);
    }
  });

const r = range(1, 10);
console.log(r.size);      // 10
console.log(r.contains(5)); // true

This lazy evaluation pattern can be extended with caching via WeakMap to avoid recomputation.


Property Observation and Change Tracking

A simple observable using the set trap:

function observe(target, onChange) {
  return new Proxy(target, {
    set(target, prop, value, receiver) {
      const result = Reflect.set(target, prop, value, receiver);
      onChange(prop, value);
      return result;
    }
  });
}

For deep observation, wrap nested objects recursively with WeakMap caching to avoid double-wrapping. This technique is the foundation of Vue 3’s reactive() system, which replaced Vue 2’s Object.defineProperty-based approach.


Revocable Proxies

Proxy.revocable creates a proxy that can be permanently disabled:

const { proxy, revoke } = Proxy.revocable(target, handler);
// use proxy normally...
revoke();
// any subsequent operation throws TypeError

This is useful for granting temporary access to sensitive resources and enforcing cleanup contracts.


Production Patterns: Vue 3, MobX, Immer

Vue 3 uses Proxy in its reactive() system. The get trap tracks property access to build dependency graphs, and set triggers re-renders. MobX 6+ similarly replaced defineProperty with Proxy for observable(). Immer.js uses Proxy to produce immutable drafts with copy-on-write semantics — set marks modifications, and get creates nested drafts on access.


Performance Considerations

Each trapped operation carries overhead. Avoid proxying hot-path objects in tight loops. Apply proxies selectively — “proxy inside, not outside” — and profile before optimizing. Deep proxies with large WeakMap caches can also impact memory usage.

Proxy is the right tool for validation, reactivity, and virtual resources. For performance-critical paths, consider whether a simpler pattern suffices. When applied judiciously, Proxy enables metaprogramming patterns that would be impossible or cumbersome with any other JavaScript feature.