Handlers for Operators

get and set intercept property reads and assignment, has intercepts in, and deleteProperty intercepts delete: enough for validation, defaults, change notification and hidden keys (Vue 3 5,482 's reactive() is built on get and set). Pass receiver on to Reflect so inherited getters and setters see the proxy as this.

A validating, observable object that hides underscore keysJavaScriptLive
const isPrivate = (key) => String(key).startsWith("_");
function observable(target, onChange) {
  return new Proxy(target, {
    get: (obj, key, rcv) => (key in obj ? Reflect.get(obj, key, rcv) : `<no ${String(key)}>`),
    set(obj, key, value, rcv) {
      if (key === "age" && !Number.isInteger(value)) throw new TypeError(`bad age: ${value}`);
      onChange(key, value);
      return Reflect.set(obj, key, value, rcv);
    },
    has: (obj, key) => !isPrivate(key) && key in obj,
    deleteProperty: (obj, key) => !isPrivate(key) && Reflect.deleteProperty(obj, key),
  });
}
const user = observable({ name: "Ada", _token: "x1" }, (k, v) => console.log(`${k} -> ${v}`));
user.age = 36;
console.log(user.name, user.email, "_token" in user, Reflect.deleteProperty(user, "_token"));
try { user.age = "old"; } catch (e) { console.log(e.message); }
try { delete user._token; } catch (e) { console.log(e.message); }

A set or deleteProperty trap returning false makes Reflect report false and strict-mode code (every module, see Strict Mode) throw. Writes made directly on the target bypass the traps, so hand out only the proxy.