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.
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); }age -> 36 Ada <no email> false false bad age: old 'deleteProperty' on proxy: trap returned falsish for property '_token'
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.