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:

  1. let target = {};
  2. 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:

  1. trapTarget - the object from which the property is read (the proxy’s target)
  2. key - the property key (a string or symbol) to read
  3. receiver - 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:

  1. let proxy = new Proxy({}, {
  2. get(trapTarget, key, receiver) {
  3. if (!(key in receiver)) {
  4. throw new TypeError("Property " + key + " doesn't exist.");
  5. }
  6. return Reflect.get(trapTarget, key, receiver);
  7. }
  8. });
  9. // adding a property still works
  10. proxy.name = "proxy";
  11. console.log(proxy.name); // "proxy"
  12. // nonexistent properties throw an error
  13. 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.