🚀AdvancedAdvanced Patterns

Proxy & Reflect

Intercept and redefine fundamental object operations using Proxy traps, and maintain correct semantics with the Reflect API.

Overview

Proxy is one of JavaScript's most powerful metaprogramming features. It wraps a target object in a handler, and every fundamental operation on the proxy getting a property, setting a value, calling the object as a function, checking with instanceof can be intercepted by a trap method on the handler. This makes it possible to build validation layers, reactive systems, access-controlled objects, and even virtual objects that compute properties on demand, all without modifying the underlying target. The Reflect API is the companion to Proxy. It provides a function for every Proxy trap, forwarding operations to the target with correct semantics. Inside a trap, you should almost always call the equivalent Reflect method rather than performing the operation manually. Reflect respects the receiver argument (the original this context), handles non-writable and non-configurable properties correctly, and returns success/failure as boolean values instead of throwing behaviour you must replicate manually if you bypass it. Practical uses of Proxy include Vue 3's reactivity system (a Proxy wraps data objects and triggers re-renders on set), Immer's produce API (a Proxy records mutations for structural sharing), negative array indexing, auto-vivification (deeply nested property access without null checks), and validation decorators. Understanding Proxy requires thinking about JavaScript's object model at the meta-object protocol level the layer that specifies how operations like [[Get]] and [[Set]] work.

Key Points

  • •new Proxy(target, handler) creates a proxy; operations on the proxy are intercepted by handler traps
  • •The 13 proxy traps correspond to internal methods: get, set, has, deleteProperty, apply, construct, and more
  • •Reflect provides a method for every trap always use Reflect inside traps to forward operations correctly
  • •The receiver parameter in get/set traps is crucial for prototype chains pass it to Reflect to preserve this
  • •A Proxy can be revoked with Proxy.revocable() after revocation, any access throws a TypeError
  • •Traps for non-configurable, non-writable properties have invariants the runtime enforces violating them throws
  • •Proxies are transparent: instanceof, typeof, and the original object's prototype chain all work through them
  • •set traps must return true (success) or false/throw in strict mode forgetting this causes silent failures

Examples

Validation proxy enforce property types on a schema

function createValidated(schema) {
  return new Proxy({}, {
    set(target, prop, value, receiver) {
      if (prop in schema) {
        const expected = schema[prop];
        if (typeof value !== expected) {
          throw new TypeError(
            `Property "${prop}" must be ${expected}, got ${typeof value}`
          );
        }
      }
      return Reflect.set(target, prop, value, receiver);
    },
    get(target, prop, receiver) {
      const val = Reflect.get(target, prop, receiver);
      return val === undefined && prop in schema ? null : val;
    }
  });
}

const user = createValidated({ name: 'string', age: 'number' });
user.name = 'Alice';  // OK
user.age = 30;        // OK

try {
  user.age = '30';    // TypeError: Property "age" must be number, got string
} catch (e) {
  console.error(e.message);
}

console.log(user.name); // Alice
console.log(user.score); // null (not in schema but handled)

Reflect.set returns a boolean indicating success. Always return it from the set trap so non-writable properties still throw in strict mode.

Reactive object with change tracking (simplified Vue 3 pattern)

function reactive(target, onChange) {
  const handler = {
    get(obj, key, receiver) {
      const value = Reflect.get(obj, key, receiver);
      // Recursively wrap nested objects
      if (value !== null && typeof value === 'object') {
        return reactive(value, onChange);
      }
      return value;
    },
    set(obj, key, value, receiver) {
      const oldValue = obj[key];
      const result = Reflect.set(obj, key, value, receiver);
      if (result && oldValue !== value) {
        onChange(key, oldValue, value);
      }
      return result;
    }
  };
  return new Proxy(target, handler);
}

const state = reactive({ count: 0, user: { name: 'Bob' } }, (key, from, to) => {
  console.log(`${key}: ${from} -> ${to}`);
});

state.count++;         // count: 0 -> 1
state.user.name = 'Alice'; // name: Bob -> Alice

Because nested objects are wrapped lazily (only when accessed), this avoids the overhead of deep upfront proxying.

Revocable proxy and negative-index array access

// Revocable proxy security / session tokens
const { proxy: secret, revoke } = Proxy.revocable(
  { token: 'abc123', expiry: Date.now() + 60000 },
  {}
);

console.log(secret.token); // abc123
revoke();

try {
  console.log(secret.token); // TypeError: Cannot perform 'get' on a revoked proxy
} catch (e) {
  console.error(e.message);
}

// Negative array indexing
function negativeIndex(arr) {
  return new Proxy(arr, {
    get(target, prop, receiver) {
      const idx = Number(prop);
      if (!isNaN(idx) && idx < 0) {
        return Reflect.get(target, target.length + idx, receiver);
      }
      return Reflect.get(target, prop, receiver);
    }
  });
}

const a = negativeIndex([10, 20, 30, 40]);
console.log(a[-1]); // 40
console.log(a[-2]); // 30
console.log(a.length); // 4

Proxy.revocable() is ideal for capability objects hand out a proxy, then revoke access when the session or permission expires.

Common Mistakes

  • Omitting the receiver argument when calling Reflect.get/set this breaks prototype-chain behaviour when a getter or setter is inherited
  • Forgetting to return true from the set trap in non-strict mode this silently fails; in strict mode it throws a TypeError
  • Trying to violate property descriptor invariants (returning a different value for a non-writable, non-configurable property) the runtime throws even if your trap tries to lie
  • Expecting Proxy to make an object deeply reactive automatically nested objects must be wrapped individually, typically lazily on access

Note

Always pair Proxy with Reflect: for every trap foo, call Reflect.foo(...arguments) as the default forwarding path. This ensures your proxy is transparent for all operations you do not intentionally override, and correctly handles edge cases in the spec like non-configurable property invariants.

Exercises

Write JavaScript code to solve each exercise. Run it and check your output.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

Why should you use Reflect.get(target, prop, receiver) instead of target[prop] inside a Proxy get trap?