Behind the Scenes: How Typescript Decorators Operate

Netanel Basal
Netanel Basal
Published in
6 min readJul 16, 2019

--

I remember the first time I saw this neat symbol. Yes, you know what I’m talking about. The @ sign, above or before class, property or a method — a decorator.

Decorators are a mechanism for observing, modifying, or replacing classes, methods, or properties in a declarative fashion.

Decorators were a proposed as a standard back in ECMAScript2016, and they’re currently in stage 2 (draft). In Typescript, we can enable them by setting the experimentalDecorators compiler flag. In this article, we’ll take a look under the hood, and learn how the typescript compiler transforms decorators to a native JS code.

We’ll focus on the three most common decorators — class decorators, method decorators, and property decorators. Let’s get started.

Class Decorators

A Class decorator is a function that takes the constructor of the class as its only parameter. If the class decorator returns a value, it will replace the class declaration with the provided constructor function, i.e., override the constructor; Otherwise, the class declaration will use the original constructor.

Let’s see an example from the official documentation:

The Object.seal() method seals an object, preventing new properties from being added to it and marking all existing properties as non-configurable.

Now, let’s examine the transpiled code generated by typescript:

First, we can see that when we target ES5, which is the standard option nowadays, typescript changes the class syntactic-sugar to a plain function, so it could work on every browser.

Next, we can see a new function named __decorate that typescript has created. The first parameter that it takes is an array of decorators. Additional parameters may vary, depending on the decorator type.

In our case — the class decorator — it’s invoked with one additional parameter, which is the class constructor.

Let’s examine the __decorate function:

Nice and clear, right? Don’t worry; it’s not as complicated as it looks. Let me break it down for you: I’ll simplify this function a bit so we could understand what’s going on.

First, it checks whether the environment supports Reflect.decorate. If that’s the case it uses it; otherwise, it falls back to its own implementation, which we’ll cover now.

We’ll start by focusing on the class decorator case:

We can see that in this case (we’ll expand on other cases later on), the __decorate function takes two parameters: the array of decorators, and the target, which is either the class constructor or the class prototype.

Then TypeScript checks if less than three arguments were passed; If that’s the case, it assumes that this is a class decorator, and sets the descriptorOrTarget variable to be the class constructor (in our case, Greeter).

The array of decorators is iterated over in reverse order. This is because when multiple decorators are applied in a single declaration, their evaluation is similar to that of function composition in mathematics. For example, the following code:

is equivalent to decoratorTwo(decoratorOne(Greeter)).

Finally, in the case of the class decorator, the decorator function is called with the class constructor as its parameter. If a value is returned, it will replace the class declaration; Otherwise, the class will remain as is.

In our case, we don’t override the original constructor, but if we want to, we can easily achieve this by extending the original constructor using a class expression:

Hopefully now you can understand what’s going on in our current example:

It’s also worth mentioning that we can tell from this example that the decorator function is called exactly once at run-time, and it’s not dependent on the number of class instances.

Method Decorators

A method decorator is a function that takes three parameters; The target — which is either the constructor function of the class for a static member, or the prototype of the class for an instance member; The key — the method name; And the descriptor — which is the property descriptor for the method.

If the decorator returns a value, it will be used as the new Property Descriptor for the method; Otherwise, the original descriptor will remain.

Let’s take a simple example from the official docs:

We modify the enumerable property of the property descriptor, and set it to false. The transpiled version of the above code is:

We can see that the method decorator function takes the following parameters: the class prototype, the method name, and the method’s descriptor, which is null in our example.

Let’s look at an expanded implementation of __decorate and see how it supports method decorators:

First, if we’re not dealing with a class decorator (indicated by the presence or 3 or more arguments), we check whether the passed descriptor is null; If that’s the case, we obtain a reference to the property descriptor by using the getOwnPropertyDescriptor method, passing it the class prototype and the method name.

Next, we invoke the decorator function, passing it the class prototype, the method name and the descriptor.

Because we have the option to return either a new descriptor or modify the existing one, we need to check whether the decorator function returns a value; If it does, that value will be used as the new descriptor; Otherwise, we use the original one.

Here’s an example of a method decorator which returns a new descriptor:

Finally, we call the Object.defineProperty() method and to set the descriptor we received as the new method descriptor. This static method defines a new property directly on an object, or modifies an existing property on an object, and returns the object.

Property Decorators

A property decorator is declared just before a property declaration. It’s a function that takes two parameters: The target— which is either the constructor function of the property’s class for a static member or the prototype of the class for an instance member. The second parameter is a key, which is the property name.

Similar to method decorators, if the property decorator returns a value, it will be used as the new Property Descriptor for the property; Otherwise, the property remains unaffected.

Let’s see an example of how to decorate a property:

The logProperty function redefines the decorated property on the object. We can define the new property in the constructor’s prototype by using the Object.defineProperty() method.

Here, we’re using a getter and a setter to intercept the values and log them to the console.

The transpiled version of the above code is:

We can see that the __decorate() call is similar to the one performed for the method decorator, with the exception of the last parameter, which in this case is set to void 0 (undefined) instead of null.

So in this case, if we re-examine the __decorate method:

Now the descriptorOrTarget isn’t assigned a value before we iterate over the decorator functions, and it remains undefined in our example, since the logProperty property decorator doesn’t return a new property descriptor.

As a result the logProperty decorator modifies the class prototype, but no additional modifications are made, since the if condition at the end isn’t met.

Alternatively, we can return a new property descriptor from our decorator function:

In this case, the property descriptor we return will be used as the third parameter in the defineProperty method call. The result of using both versions of the logProperty decorator will be identical.

The observant among you have probably notice the one pitfall we have when using property decorators; Typescript always defines the property on the class prototype instead of on the instance. This behavior causes every instance to share the same value:

We can overcome this issue by creating a unique key and adding it on the instance using the getter and setter functions. For example:

We generate a new key since we can’t use the original property key, as it will lead to an infinite loop. Now, each instance can have a different value for this property.

🚀 In Case You Missed It

Here are a few of my open source projects:

  • Akita: State Management Tailored-Made for JS Applications
  • Spectator: A Powerful Tool to Simplify Your Angular Tests
  • Transloco: The Internationalization library Angular
  • Forms Manger: The Foundation for Proper Form Management in Angular

Follow me on Medium or Twitter to read more about Angular, Akita and JS!

--

--

A FrontEnd Tech Lead, blogger, and open source maintainer. The founder of ngneat, husband and father.