Object Shape Validation Using the get
Trap
One of the interesting, and sometimes confusing, aspects of JavaScript is that reading nonexistent properties doesn’t throw an error. Instead, the value undefined
is used for the property value, as in this example:
let target = {};
console.log(target.name); // undefined
In most other languages, attempting to read target.name
throws an error because the property doesn’t exist. But JavaScript just uses undefined
for the value of the target.name
property. If you’ve ever worked on a large code base, you’ve probably seen how this behavior can cause significant problems, especially when there’s a typo in the property name. Proxies can help you save yourself from this problem by having object shape validation.
An object shape is the collection of properties and methods available on the object. JavaScript engines use object shapes to optimize code, often creating classes to represent the objects. If you can safely assume an object will always have the same properties and methods it began with (a behavior you can enforce with the Object.preventExtensions()
method, the Object.seal()
method, or the Object.freeze()
method), then throwing an error on attempts to access nonexistent properties can be helpful. Proxies make object shape validation easy.
Since property validation only has to happen when a property is read, you’d use the get
trap. The get
trap is called when a property is read, even if that property doesn’t exist on the object, and it takes three arguments:
trapTarget
- the object from which the property is read (the proxy’s target)key
- the property key (a string or symbol) to readreceiver
- the object on which the operation took place (usually the proxy)
These arguments mirror the set
trap’s arguments, with one noticeable difference. There’s no value
argument here because get
traps don’t write values. The Reflect.get()
method accepts the same three arguments as the get
trap and returns the property’s default value.
You can use the get
trap and Reflect.get()
to throw an error when a property doesn’t exist on the target, as follows:
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
if (!(key in receiver)) {
throw new TypeError("Property " + key + " doesn't exist.");
}
return Reflect.get(trapTarget, key, receiver);
}
});
// adding a property still works
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
// nonexistent properties throw an error
console.log(proxy.nme); // throws error
In this example, the get
trap intercepts property read operations. The in
operator is used to determine if the property already exists on the receiver
. The receiver
is used with in
instead of trapTarget
in case receiver
is a proxy with a has
trap, a type I’ll cover in the next section. Using trapTarget
in this case would sidestep the has
trap and potentially give you the wrong result. An error is thrown if the property doesn’t exist, and otherwise, the default behavior is used.
This code allows new properties like proxy.name
to be added, written to, and read from with no problems. The last line contains a typo: proxy.nme
should probably be proxy.name
instead. This throws an error because nme
doesn’t exist as a property.