Reflect API and Proxy Patterns

The Reflect API provides methods that mirror Proxy traps and perform default operations.

Reflect API

There is a Reflect method for each Proxy trap. Each one performs the default operation:

const obj = { x: 1 };

// Property read
console.log(obj.x); // 1
console.log(Reflect.get(obj, "x")); // 1

// Property write
obj.y = 2;
Reflect.set(obj, "z", 3);

// Property delete
delete obj.y;
Reflect.deleteProperty(obj, "z");

Using Reflect in Proxy Traps

Inside a trap, a Reflect method forwards the operation to the target:

const handler = {
  get(target, property, receiver) {
    console.log(`Getting ${property}`);
    return Reflect.get(target, property, receiver);
  },
  set(target, property, value, receiver) {
    console.log(`Setting ${property} = ${value}`);
    return Reflect.set(target, property, value, receiver);
  },
};

const proxy = new Proxy({ x: 1 }, handler);
proxy.x; // Logs: "Getting x"
proxy.y = 2; // Logs: "Setting y = 2"

Why Use Reflect?

  1. Correct receiver handling: Maintains proper this binding
  2. Clear status for mutations: Methods like Reflect.set() and Reflect.deleteProperty() return booleans
  3. Consistency: Same API as Proxy traps

Some Reflect methods can still throw, for example when the target is not a valid object, so it is not true that Reflect never throws.

// Reflect.set returns boolean instead of throwing
const frozen = Object.freeze({ x: 1 });

// Throws in strict mode:
// frozen.x = 2;

// Returns false:
console.log(Reflect.set(frozen, "x", 2)); // false

Practical Examples

Validation Proxy

function createValidatedObject(schema) {
  return new Proxy(
    {},
    {
      set(target, property, value) {
        const validator = schema[property];
        if (validator && !validator(value)) {
          throw new TypeError(`Invalid value for ${property}`);
        }
        return Reflect.set(target, property, value);
      },
    },
  );
}

const user = createValidatedObject({
  name: (v) => typeof v === "string" && v.length > 0,
  age: (v) => typeof v === "number" && v >= 0,
  email: (v) => typeof v === "string" && v.includes("@"),
});

user.name = "Alice"; // OK
user.age = 30; // OK
user.email = "a@b.com"; // OK
// user.age = -5;         // TypeError
// user.email = 'invalid'; // TypeError

Logging Proxy

function withLogging(obj, name = "Object") {
  return new Proxy(obj, {
    get(target, property, receiver) {
      const value = Reflect.get(target, property, receiver);
      console.log(`${name}.${property} -> ${value}`);
      return value;
    },
    set(target, property, value, receiver) {
      console.log(`${name}.${property} = ${value}`);
      return Reflect.set(target, property, value, receiver);
    },
  });
}

const config = withLogging({ debug: false }, "config");
config.debug; // Logs: "config.debug -> false"
config.debug = true; // Logs: "config.debug = true"

Negative Array Indices

function negativeArray(arr) {
  return new Proxy(arr, {
    get(target, property, receiver) {
      if (typeof property === "string") {
        const index = Number(property);
        if (Number.isInteger(index) && index < 0) {
          const normalizedIndex = target.length + index;
          return Reflect.get(target, String(normalizedIndex), receiver);
        }
      }
      return Reflect.get(target, property, receiver);
    },
  });
}

const arr = negativeArray([1, 2, 3, 4, 5]);
console.log(arr[-1]); // 5 (last element)
console.log(arr[-2]); // 4 (second to last)