Reading a property that does not exist gives you undefined. A typo in a config key stays quiet until something further down the line breaks.
A Proxy wraps an object and lets you decide what a read or a write means. The handler methods are called traps, and the two you will use most are get and set.
Turn typos into errors
This proxy throws when you read a property the target does not have.
// reject unknown properties
const strict = target => new Proxy(target, {
get(obj, prop) {
if (!(prop in obj)) throw new ReferenceError(`No property "${String(prop)}"`);
return obj[prop];
}
});
const config = strict({ host: 'localhost', port: 8080 });
console.log(config.host);
try { config.hsot; }
catch (err) { console.log(err.constructor.name + ':', err.message); }
It prints:
localhost
ReferenceError: No property "hsot"
A misspelled key now fails where it is written, instead of producing undefined and failing somewhere else.
Check values on the way in
The set trap runs before the write. It has to return true, or strict mode code throws a TypeError.
// validate on write
const user = new Proxy({}, {
set(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) throw new TypeError('age must be an integer');
if (value < 0) throw new RangeError('age must be 0 or more');
}
obj[prop] = value;
return true; // must return true or the set throws
}
});
user.age = 30;
console.log('age ->', user.age);
try { user.age = -1; }
catch (err) { console.log(err.constructor.name + ':', err.message); }
try { user.age = 'thirty'; }
catch (err) { console.log(err.constructor.name + ':', err.message); }
It prints:
age -> 30
RangeError: age must be 0 or more
TypeError: age must be an integer
The object cannot be put into a bad state, no matter which code does the writing.
Default values
A get trap can also fill in a value for keys that are missing.
// default values
const withDefault = (obj, fallback) => new Proxy(obj, {
get: (t, p) => (p in t ? t[p] : fallback)
});
const counts = withDefault({ apples: 3 }, 0);
console.log(counts.apples, counts.pears);
It prints:
3 0
Useful for counters and lookup tables, where the missing case is always the same.
Watch what code touches
Because the trap runs on every read, a proxy is a quick way to see which properties are actually used.
// log every read
const reads = [];
const tracked = new Proxy({ x: 1, y: 2 }, {
get(t, p) { reads.push(p); return t[p]; }
});
tracked.x; tracked.y; tracked.x;
console.log('reads ->', reads);
It prints:
reads -> [ 'x', 'y', 'x' ]
This is how reactive frameworks track dependencies. They record the reads during a render and re-run when one of those properties changes.
Forward with Reflect
Writing t[p] inside a trap works for simple objects, but it loses the correct this when getters are involved. Reflect has one method per trap with matching arguments.
// forwarding correctly with Reflect
const safe = new Proxy({ a: 1 }, {
get: (t, p, receiver) => Reflect.get(t, p, receiver)
});
console.log(safe.a, 'a' in safe, Object.keys(safe));
It prints:
1 true [ 'a' ]
Use Reflect as the default action, then add your own behaviour around it.
What to remember
- A proxy wraps a target and intercepts operations through traps.
getruns on reads,setruns on writes and must returntrue.Reflect.getand friends give you the normal behaviour to fall back on.- There is a cost per access, so keep proxies out of hot loops.
Proxies are best used at the edges of a system, such as config objects and test doubles, where the extra checking pays for itself.