Proxies enable us to do runtime meta-programming by allowing us to intercept and redefine the behaviour for intrinsic operations such as property getters, setters, value assignments, call operations and so on.
Now the actual, real-world, practical good use cases for proxies are few and far between. In most cases, the same thing can be achieved with a bit of repetitive boilerplate code with far better performance. Still, proxies are great and incredibly powerful. Let’s have a look at some terrible use cases to show just how magical proxies can be.
Forgiving property names
One of the operations we can override is an object’s property getter. So let’s use that to provide an auto-correcting property lookup using the Levenshtein distance to approximate what the user’s intended property name was.
First things first, we need to define a function to return the Levenshtein distance between two strings. The Levenshtein distance is essentially a measurement of the minimum number of single-character edits (insertions, deletions or substitutions) required to change one string into the other.
We’ll do the recursive variant because it’s straightforward and easier to follow than a more optimized one. However, it should be noted that it’s also extremely inefficient compared to an iterative approach with lookup tables:
With the Levenshtein distance figured out, it’s fairly trivial to get the closest matching property name by reducing an array of property names to the string with the shortest distance to the target property:
Finally moving on to the actual proxy object, proxies are defined as objects with a target object and a handler object. The target is the object which is virtualized by the proxy and the handler is an object whose properties are traps, or functions that define the behaviour of a proxy when an operation is done to it.
So to make an object’s properties be “autocorrected” we’ll define a function that takes the target as a parameter and returns a proxy which re-defines the get trap:
Which, when in use, would yield the following:
Get traps also override the subscript operator because the member and subscript operators use this trap, meaning the following is equivalent to the above example:
Strictly typed objects
A slightly more useful variation of the previous use case would be to disallow unknown properties to be used and instead throw an error pointing out the “most likely” candidate.
We’ll re-use the same Levenshtein function as before, but instead of adding a factory function to create the proxy we’ll bake it into the class constructor by returning a proxy to the constructed object instead of the object itself:
Which, would yield the following error when a non-existing property is accessed:
Proxies are incredibly powerful and can be used and abused for a wide array of things, but it’s important to remember that proxies cannot be emulated by a pre-processor and have to be supported by the runtime itself. It’s a rare case for a feature introduced that is not backwards compatible. In most cases, we can achieve the same without proxies although it might involve a bit more boilerplate code.
Another thing to keep in mind is that using proxies isn’t free, there is a non-trivial overhead as there is another level of indirection in play. So in some cases, compile-time metaprogramming might be preferred over doing it at run-time.
Lastly, proxies, while fairly magical, do not necessarily lead to very clean and easily understandable code but they’re worth knowing about as there are certainly a few cases where they may be the best way or even the only way forward.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.