✱ Class Method Decorator

Now that we understood how we can define and configure new or existing properties of an object, let’s move our attention to decorators and why we discussed property descriptors at all.

A decorator is a JavaScript function (recommended pure function) that is used to modify class properties/methods or class itself. When you add @decoratorFunction syntax on the top of a class property, method or class itself, decoratorFunction gets called with few arguments which we can use to modify class or class properties.

As decorators are still in Stage 2 of the ECMAScript proposal process, we can’t run any JavaScript code that contains decorators inside a browser or Node as this feature most probably isn’t implemented inside the JavaScript engine. For this purpose, we need to use a transpiler such as Babel or TypeScript that can compile JavaScript code containing decorators into something else that a JavaScript engine can understand.

We are going to use Babel for simplicity. You can follow this document to install Babel and CLI inside your project.

$ npm install --save-dev @babel/core @babel/cli $ npx babel --version

7.10.4 (@babel/core 7.10.4)

With these commands, we have installed Babel v7. The @babel/core package contains the core implementation of Babel and @babel/cli package contains the command-line APIs to interface with it. You can follow this document to understand the command-line interface of babel .

$ npm install --save-dev @babel/preset-env

$ npm install --save-dev @babel/plugin-proposal-decorators

The @babel/preset-env package is preset that contains standard babel plugins. A babel plugin performs the transformation of code that contain new JavaScript features such as ES6 arrow function expressions into function declarations that can work across all browser.

A preset configures and conducts the compilation process using settings listed inside a configuration file such as .babelrc file. The @babel/preset-env contains plugins to transform standard JavaScript code containing features from ES6 or above into ES5 or below.

When a proposal is in an early stage such as decorators in this example, it is may not be included in a preset. Therefore we need to install a plugin separately. Therefore we have installed the @babel/plugin-proposal-decorators plugin separately to compile decorators.

Now we need to tell the Babel CLI to use @babel/preset-env preset and @babel/plugin-proposal-decorators plugin. Let’s create a babel.config.json in the project directory. You can read more this file from here.

{

"presets": [

"@babel/preset-env"

],

"plugins": [

[

"@babel/plugin-proposal-decorators",

{

"decoratorsBeforeExport": true

}

]

]

}

In the above example, we have compiled a sample JavaScript file babel-test.js that contains ES6 features such as arrow functions and template string literals. As you can see, our babel setup is working fine. Now it’s time to move on to implementing decorators.

Let’s create a simple readonly decorator. But before that, let’s create simple User class with getFullName method which returns the full name of the user by combining firstName and lastName .

Above code prints John Doe to the console. But there is a huge problem, anybody can modify getFullName method.

To avoid public access to override any of our methods, we need to modify property descriptor of getFullName method which lives on User.prototype object. A class method is just a property with function value so the things are pretty similar to an object property.

Now, if any hacker tries to override getFullName method, the override operation will be simply ignored. In strict mode, this operation will result in an error as we saw in earlier example.

But if we have many methods on the User class, doing this manually won’t be so great. This is where decorator comes in. We can achieve the same thing by putting @readonly annotation on top of getFullName method as below.

Have a look at readonly fuction. A decorator is nothing but a function that is called when the JavaScript runtime encounters the decorator. The target argument value of this function is an object representation of the entity onto which the decorator was added, which is getFullName method in this case.

The target object is called element according to ECMAScript proposal but we are going to call it to target for the heck of it. This target object contains the description of the element we are modifying. It could be a class method (prototype property), class field (instance field), or a class itself.

This target object looks like the following.

{

kind: 'method' | 'accessor' | 'field' | 'class',

key: '<property-name>',

descriptor: <property-descriptor>,

placement: 'prototype' | 'static' | 'own',

initializer: <function>,

...

}

The kind property indicates whether the element is a class method, class field, or something else while the key is the name of the element. You can read more about these properties from the decorators proposal document. What we are interested in is the descriptor property. This is the actual property descriptor of the element.

Since we are decorating the getFullName method, the descriptor points to the property descriptor of getFullName method. A method of a class lives on its prototype and a prototype is an object. Therefore a method of the class is the property of its prototype and its value is a function.

From within a decorator function, we have to return the target object back at any cost. But before we do that, we can change the descriptor of the target. This descriptor will replace the existing property descriptor of that property. In the above example, we have made the getFullName property (method) readonly by setting descriptor.writable to false .

But when we tried to run this program with node , Node.js simply can’t recognize the @readonly syntax. That’s why we have set up Babel. Let’s compile this program using babel and then run it using Node.

Now when we compile the code with Babel and run the output file with Node, we get the expected error from runtime that the getFillName property is not something that you can write because it is read-only.

In the above example, we have logged the target object decorator is decorating. As we can see, the kind is method and key is the getFullName method name. The placement is prototpe since it lives on the prototype of the class and descriptor contains a function value among others.

There is another version of decorator annotation that goes like @decorator( ...args ) . Here the args are the custom arguments passed to the decoration. Since this is a decorator call, the decorator function must return a function that decorates the target. This is also called the decorator factory since this function call returns an actual decorator.

When a class method is static , the method lives on the class itself instead on its prototype . Let’s add a static method to the User class.

In the above example, the User class has the getVersion method but since its property descriptor sets the writable to true by default, any intruder can override it. Let’s create the same old @readonly decorator.