Virtualizing TypeScript Class Properties With Proxies and Decorators

Michael Michlin
The Startup

--

Recently, while developing my pet project I encountered a problem where I needed to “virtualize” a property in a TypeScript class and I thought it would be interesting to share my solution.

The Problem

I have a base class and a derived class. The base class defines a property — let’s call it first — and assigns a value to it (assume that it is of an object type and not of a primitive type — we will talk about primitive types later). The base class also defines another property — let’s call it second— which makes use of the first property. The derived class overrides the first property and gives it a different value. Then the instance of the derived class is created and the value of the second property is read. Here is the code snippet illustrating the situation:

When I first wrote this code I was expecting the number 2 in the console — after all it is the derived object that I have created and by the time I am doing console.log, the first property has been assigned the value of {v:2}. To my surprise the console showed 1 and not 2.

After a second, it became clear that the result was obviously correct and it was my thinking that was not. Since the properties are initialized as they are declared, the code is actually part of the constructor. During the construction of the Base class the value of this.first on line 4 is {v:1}, which is remembered in the second property and remains unchanged when it’s printed on line 13.

So it works as it should (or as a colleague of mine once put it “works as implemented”), but that left me unsatisfied because I really liked the clear structure of the code with property initializers but I also really wanted the overridden value of the first property to show up when I access the second property.

The Solution

I realized that I needed to postpone access to the first property during the assignment of its value to the second property until the moment the second property is read. The simplest way to postpone access to a value is to have a function that returns this value. So I wrote the following code:

This works but the obvious — and major — problem is that the type of the second property ceases to be the same as the type of the first property — it is now a function.

If only JavaScript had an object that would present itself as a target object, but allow custom code to be executed when accessed! Of course, JavaScript does have such an object and it is called Proxy. You create a proxy instance, giving it a target object and a handler object. The latter has methods that are invoked when the proxy instance is accessed.

Using the Proxy object, the idea is that whenever the first property is assigned a value, we will create a proxy object for this value. We will use the same instance of the handler for all these proxy objects and it will keep the latest value that was assigned to our property. When the handler is invoked in order to read the value, it will return the value it holds at the time of access and not the one that the proxy was created with.

So the next attempt went like this:

This works but the code is quite unwieldy. What if I needed another property to be “virtualized”? I would have to create another instance of the VirtHandler class and keep it in another property. Fortunately, TypeScript has decorators that can perform exactly this kind of work behind the scenes.

I created the @virtual decorator and put it, along with the proxy handler, into a separate file — to show just how simple it is to make the property “virtual” once you have the infrastructure in place.

Now, whenever we need a “virtualized” property, we need only to apply the @virtual decorator to it — the rest of the code and the way the property is accessed remain exactly the same.

Primitive Types

I used object types in the previous discussion for two simple reasons: first, that was my initial need, and second, the above solution wouldn’t work for the primitive types: strings, numbers and booleans. In fact, if you were to try assigning a number to the first property as in the code below, you would get the following error: TypeError: Cannot create proxy with a non-object as target or handler.

What we can do in order to make it work is to box the primitive values before passing them to the proxy. Boxing means creating instances of the built-in classes corresponding to the primitive values: Number for numbers, String for strings and Boolean for booleans. The proxy handler’s get method will be invoked when the unboxing occurs; that is, when the object needs to be converted back to the primitive value. For example, for the Number object, the get method will be asked to return the valueOf method.

There is a caveat here, however: within the proxy handler’s get method it is not enough to just use the expression this.v[p]. The retrieved method should be bound to the target object first. A good explanation of why this has to be done can be found here.

An extra complication arises when the virtualized property is assigned a null or undefined value. We will keep this value in our handler, but what would happen when the property is used? In most cases, the behavior would be correct — for example, if we try to get property value (e.g. obj.b.v) the correct exception will be thrown.

There is one particular case that deserves a special attention: when our virtualized property is used in an arithmetic expression or is appended to a string. JavaScript will try to get the value of the Symbol.toPrimitive well-known symbol and we must return a proper function so that the behavior would be the same as expected from a regular null or undefined value.

Other Operations

So far, we were focused on a single operation on our virtual property — getting values of its properties. The handler’s get method took care of that. But what if our virtual property is a function? What if we want to find out whether it is an instance of a particular class? If we want our property to support all other possible operations, we need to implement all methods specified in the ProxyHandler interface. Fortunately, it is very easy to do so. JavaScript provides the Reflect object that has methods matching those of a proxy handler, and we just need to call the corresponding methods of the Reflect object, passing our latest value as the first parameter.

Final Code

With all of the above in mind, below is the implementation of the proxy handler and the decorator that both support object types, primitive types and all proxy operations.

Note that we only create a proxy object once. When a new value is assigned to the property it is kept in the handler and since all proxy methods are intercepted, they will always work with the latest remembered value.

Also note that in the decorator’s get method we check whether we have already created our handler and proxy objects and create them if not. This is necessary to handle the situation when the property is initially undefined or null.

Conclusion

The problem I described here might not be a widespread one and it only manifests itself if you have a hierarchy of classes with property initializers. If, however, you do encounter such a structure, the described solution is one more tool in your arsenal.

--

--