Legacy (Deprecated)

LEGACY: A minimal guide to JavaScript (ECMAScript) Decorators and Property Descriptor of the Object

A short introduction of ECMAScript “decorators” proposal with basic examples and a little bit about the ECMAScript ecosystem.

Uday Hiwarale
JsPoint
Published in
15 min readJul 15, 2020

--

⚠️ This article has been revised recently due to the change in the ECMAScript decorators proposal. You can read the latest version of this article from here.

Why ECMAScript Decorators instead of JavaScript Decorators in the title? Because ECMAScript is a standard for writing scripting languages like JavaScript. It doesn’t enforce JavaScript to support all the specs but a JavaScript engine (implemented inside a browser) may or may not support a feature introduced in ECMAScript or support with little different behavior.

Consider ECMAScript as a language that you speak such as English. Then JavaScript would be a dialect like British English. A dialect is a language in itself but it is based on principals of the language it was derived from. So, ECMAScript is a cookbook for cooking/writing JavaScript and it’s up to the chef/developer to follow all ingredients/rules or not.

Generally, JavaScript adopters (such as web browsers or server-side platforms such as Node.js) implement all the specifications written in the language (else the whole point of having a standard crumbles) and usually ship it with a beta-preview version to make sure if the implementation is stable.

TC39 or Technical Committee 39 at ECMA International is responsible for maintaining the ECMAScript standard. Members of this team belong to ECMA International, browser vendors, and companies interested in the web industry in general such a Google, Mozilla, etc.

As ECMAScript is an open standard, anybody can suggest new ideas or features that would be a great addition to the language. Therefore, a proposal for a new feature goes through 4 main stages and TC39 gets involved in this process until that feature is approved and included to the standard.

+-------+-----------+----------------------------------------+
| stage | name | mission |
+-------+-----------+----------------------------------------+
| 0 | strawman | Present a new feature (proposal) |
| | | to TC39 committee. Generally presented |
| | | by TC39 member or TC39 contributor. |
+-------+-----------+----------------------------------------+
| 1 | proposal | Define use cases for the proposal, |
| | | dependencies, challenges, demos, |
| | | polyfills etc. A champion |
| | | (TC39 member) will be |
| | | responsible for this proposal. |
+-------+-----------+----------------------------------------+
| 2 | draft | This is the initial version of |
| | | the feature that will be |
| | | eventually added. Hence description |
| | | and syntax of feature should |
| | | be presented. A transpiler such as |
| | | Babel should support and |
| | | demonstrate implementation. |
+-------+-----------+----------------------------------------+
| 3 | candidate | Proposal is almost ready and some |
| | | changes can be made in response to |
| | | critical issues raised by adopters |
| | | and TC39 committee. |
+-------+-----------+----------------------------------------+
| 4 | finished | The proposal is ready to be |
| | | included in the standard. |
+-------+-----------+----------------------------------------+

Right now (as of June 2018), Decorators are in stage 2 and we have Babel plugin to transpile decorators babel-plugin-transform-decorators-legacy. In stage 2, as the syntax of the feature is subjected to change, it’s not recommended to use it in production as of now. In any case, decorators are beautiful and very useful to achieve things quicker.

From here on, we are working on a JavaScript feature that is still considered experimental by JavaScript engines, so your Node.js version might not support it. Therefore, we need Babel or TypeScript transpiler to convert decorator syntax into vanilla JavaScript syntax. Use the js-plugin-starter plugin to set up a very basic project. I have configured this boilerplate to work well with what we are going to cover in this article.

To understand decorators, we need to first understand what is a property descriptor of a JavaScript object property. A property descriptor is a set of rules on an object property, like whether a property is writable or enumerable. When we create a simple object and add some properties to it, each property has a default property descriptor.

var myObj = {
myPropOne: 1,
myPropTwo: 2
};

myObj is a simple JavaScript object which looks like below in the console.

Now, if we write a new value to myPropOne property like below, the operation will be successful and we will get the changed value.

myObj.myPropOne = 10;
console.log( myObj.myPropOne ); //==> 10

To get the property descriptor of property, we need to use Object.getOwnPropertyDescriptor(obj, propName) method. Own here means return the property descriptor of propName property only if that property belongs to the object obj and not on its prototype chain.

let descriptor = Object.getOwnPropertyDescriptor(
myObj,
'myPropOne'
);
console.log( descriptor );

Object.getOwnPropertyDescriptor method returns an object with keys describing the permissions and current state of the property. value is the current value of the property, writable is whether the user can assign a new value to the property, enumerable is whether this property will show up in enumerations like for in loop or for of loop or Object.keys etc.

configurable is whether the user has permission to change property descriptor and make changes to writable and enumerable. Property descriptor also has get and set keys which are middleware functions to return value or update value, but these are optional.

To create a new property on an object or update existing property with a custom descriptor, we use Object.defineProperty. Let’s modify an existing property myPropOne with writable set to false, which should disable writes to myObj.myPropOne.

'use strict';var myObj = {
myPropOne: 1,
myPropTwo: 2
};
// modify property descriptor
Object.defineProperty( myObj, 'myPropOne', {
writable: false
} );
// print property descriptor
let descriptor = Object.getOwnPropertyDescriptor(
myObj, 'myPropOne'
);
console.log( descriptor );
// set new value
myObj.myPropOne = 2;

As you can see from the above error, our property myPropOne is not writable, hence if a user is trying to assign a new value to it, it will throw an error.

If Object.defineProperty is updating existing property descriptor, then original descriptor will be merged with new modifications. Object.defineProperty returns the original object myObj after changes.

Let’s see what will happen if we set enumerable descriptor key to false.

var myObj = {
myPropOne: 1,
myPropTwo: 2
};
// modify property descriptor
Object.defineProperty( myObj, 'myPropOne', {
enumerable: false
} );
// print property descriptor
let descriptor = Object.getOwnPropertyDescriptor(
myObj, 'myPropOne'
);
console.log( descriptor );
// print keys
console.log(
Object.keys( myObj )
);

As you can see from the above result, we can’t see myPropOne property of the object in Object.keys enumeration.

When you define a new property on object using Object.defineProperty and pass empty {} descriptor, the default descriptor looks like below.

Now, let’s define a new property with custom descriptor where configurable descriptor key is set to false. We will keep writable to false and enumerable to true with value set to 3.

var myObj = {
myPropOne: 1,
myPropTwo: 2
};
// modify property descriptor
Object.defineProperty( myObj, 'myPropThree', {
value: 3,
writable: false,
configurable: false,
enumerable: true
} );
// print property descriptor
let descriptor = Object.getOwnPropertyDescriptor(
myObj, 'myPropThree'
);
console.log( descriptor );
// change property descriptor
Object.defineProperty( myObj, 'myPropThree', {
writable: true
} );

By setting configurable descriptor key to false, we lost the ability to change descriptor of our property myPropThree. This is very helpful if you don’t want your users to manipulate the recommended behavior of an object.

get (getter) and set (setter) for a property can also be set in property descriptor. But when you define a getter, it comes with some sacrifices. You can not have an initial value or value key on the descriptor at all because the getter will return the value of that property. You can not use writable key on descriptor as well, because your writes are done through the setter and you can prevent writes there. Have a look at MDN documentation of getter and setter, or read this article because they don’t need much explanation here.

You can create and/or update multiple properties at once using Object.defineProperties which takes two arguments. First argument is target object on which properties has to be added/modified and second argument is object with key as property name and value as its property descriptor. This function returns the target object.

Have you tried Object.create function to create objects? This is the easiest way to create an Object with no or a custom prototype. It is also one of the easier ways to create objects from scratch with custom property descriptors.

Object.create function has the following syntax.

var obj = Object.create( prototype, { property: descriptor, ... } )

Here prototype is an object which will be the prototype of the obj. If prototype is null, then obj won’t have any prototype. When you define an empty or non-empty object with var obj= {} syntax, by default, obj.__proto__ points to Object.prototype hence obj has the prototype of Object class.

This is similar to using Object.create with Object.prototype as the first argument (prototype of the object being created).

'use strict';var o = Object.create( Object.prototype, {
a: { value: 1, writable: false },
b: { value: 2, writable: true }
} );
console.log( o.__proto__ );
console.log(
'o.hasOwnProperty( "a" ) => ', o.hasOwnProperty( "a" )
);

But when we set the prototype to null, we get below error.

'use strict';var o = Object.create( null, {
a: { value: 1, writable: false },
b: { value: 2, writable: true }
} );
console.log( o.__proto__ );
console.log(
'o.hasOwnProperty( "a" ) => ', o.hasOwnProperty( "a" )
);

✱ 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.

Let’s create a simple readonly decorator function. 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.

class User {
constructor( firstname, lastName ) {
this.firstname = firstname;
this.lastName = lastName;
}
getFullName() {
return this.firstname + ' ' + this.lastName;
}
}
// create instance
let user = new User( 'John', 'Doe' );
console.log( user.getFullName() ); // John Doe

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

User.prototype.getFullName = function() {
return 'HACKED!';
}

With this, now we get the below result.

HACKED!

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.

Object.defineProperty( User.prototype, 'getFullName', {
writable: false
} );

Now, if any user is trying to override getFullName method, he/she will get below error.

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.

function readonly( target, property, descriptor ) {
descriptor.writable = false;
return descriptor;
}
class User {
constructor( firstname, lastName ) {
this.firstname = firstname;
this.lastName = lastName;
}
@readonly
getFullName() {
return this.firstname + ' ' + this.lastName;
}
}
User.prototype.getFullName = function() {
return 'HACKED!';
}

Have a look at readonly method. It accepts three arguments. property is name of the property/method which belongs to target object (which is same as User.prototype) and descriptor is the property descriptor for that property. From within a decorator function, we have to return the descriptor at any cost. This descriptor will replace the existing property descriptor of that property.

There is another version of decorator syntax which goes like @decoratorWrapperFunction( ...customArgs ). But with this syntax, decoratorWrapperFunction should return a decoratorFunction which is the same as used in the previous example.

function log( logMessage ) {
// return decorator function
return function ( target, property, descriptor ) {
// save original value, which is method (function)
let originalMethod = descriptor.value;
// replace method implementation
descriptor.value = function( ...args ) {
console.log( '[LOG]', logMessage );
// here, call original method
// `this` points to the instance
return originalMethod.call( this, ...args );
};
return descriptor;
}
}
class User {
constructor( firstname, lastName ) {
this.firstname = firstname;
this.lastName = lastName;
}
@log('calling getFullName method on User class')
getFullName() {
return this.firstname + ' ' + this.lastName;
}
}
var user = new User( 'John', 'Doe' );
console.log( user.getFullName() );

Decorators do not differentiate between static and non-static methods. The below code will work just fine, the only thing that will change is how you access the method. The same applies to Instance Field Decorators which we will see next.

@log('calling getVersion static method of User class')
static getVersion() {
return 'v1.0.0';
}
console.log( User.getVersion() );

✱ Class Instance Field Decorator

So far, we have seen changing property descriptor of a method with @decorator or @decorator(..args) syntax, but what about public/private properties (class instance fields)?

Unlike typescript or java, JavaScript classes do not have class instance fields AKA class properties. This is because anything defined in the class and outside the constructor should belong to the class prototype. But there is a new proposal to enable class instance fields with public and private access modifiers, which is now in stage 3, and we have babel transformer plugin for it.

Let’s define a simple User class but this time, we don’t need to set default values for firstName and lastName from within the constructor.

class User {
firstName = 'default_first_name';
lastName = 'default_last_name';
constructor( firstName, lastName ) {
if( firstName ) this.firstName = firstName;
if( lastName ) this.lastName = lastName;
}
getFullName() {
return this.firstName + ' ' + this.lastName;
}
}
var defaultUser = new User();
console.log( '[defaultUser] ==> ', defaultUser );
console.log( '[defaultUser.getFullName] ==> ', defaultUser.getFullName() );
var user = new User( 'John', 'Doe' );
console.log( '[user] ==> ', user );
console.log( '[user.getFullName] ==> ', user.getFullName() );

Now, if you check prototype of User class, you won’t be able to see firstName and lastName properties.

Class instance fields are a very helpful and important part of Object-Oriented Programming (OOP). It’s good that we have a proposal for that but the story is far from over.

Unlike class methods that live on the class prototype, class instance fields live on object/instance. Since the class instance field is neither part of the class nor its prototype, it’s a little tricky to manipulate its descriptor. Babel gives us initializer function on the property descriptor of the class instance field instead of value key. Why initializer function instead of value, this topic is in debate and since Decorators are in stage-2, no final draft has been published to outline this but you can follow this answer on Stack Overflow to understand the background story.

That being said, let’s modify our early example and create a simple @upperCase decorator that will change the case of the class instance field’s default value.

function upperCase( target, name, descriptor ) {
let initValue = descriptor.initializer();
descriptor.initializer = function(){
return initValue.toUpperCase();
}
return descriptor;
}
class User {

@upperCase
firstName = 'default_first_name';

lastName = 'default_last_name';
constructor( firstName, lastName ) {
if( firstName ) this.firstName = firstName;
if( lastName ) this.lastName = lastName;
}
getFullName() {
return this.firstName + ' ' + this.lastName;
}
}
console.log( new User() );

We can also make use of the decorator function with parameters to make it more customizable.

function toCase( CASE = 'lower' ) {
return function ( target, name, descriptor ) {
let initValue = descriptor.initializer();

descriptor.initializer = function(){
return ( CASE == 'lower' ) ?
initValue.toLowerCase() : initValue.toUpperCase();
}

return descriptor;
}
}
class User {
@toCase( 'upper' )
firstName = 'default_first_name';
lastName = 'default_last_name';
constructor( firstName, lastName ) {
if( firstName ) this.firstName = firstName;
if( lastName ) this.lastName = lastName;
}
getFullName() {
return this.firstName + ' ' + this.lastName;
}
}
console.log( new User() );

descriptor.initializer function is used internally by Babel to create value of property descriptor of an object property. This function returns the initial value assigned to the class instance field. Inside the decorator, we need to return another initializer function that returns the final value.

Class instance field proposal is highly experimental and there is a strong chance that it’s syntax might change until it goes to stage-4. Hence, it’s not a good practice to use class instance fields with decorators yet.

✱ Class Decorator

Now we are familiar with what decorators can do. They can change the properties and behavior of class methods and class instance fields, giving us the flexibility to dynamically achieve those things with a simpler syntax.

Class decorators are a little bit different than decorators we saw earlier. Previously, we used property descriptors to modify the behavior of a property or method, but in the case of a class decorator, we need to return a constructor function.

Let’s understand what a constructor function is. Underneath, a JavaScript class is nothing but a function that is used to add prototype methods and define some initial values for the fields.

function User( firstName, lastName ) {
this.firstName = firstName;
this.lastName = lastName;
}
User.prototype.getFullName = function() {
return this.firstName + ' ' + this.lastName;
}
let user = new User( 'John', 'Doe' );console.log( user );
console.log( user.__proto__ );
console.log( user.getFullName() );

Here is a great article to understand this in JavaScript.

So when we call new User, User function is invoked with arguments we passed and in return, we got an object. Hence, User is a constructor function. BTW, every function in JavaScript is a constructor function, because if you check function.prototype, you will get constructor property. As long as we are using new keyword with a function, we should expect an object in return.

If you return a valid JavaScript Object from constructor function, then that value will be used instead of creating new object using this assignments. That will break the prototype chain though because retuned object won’t have any prototype methods of constructor function.

With that in mind, let’s focus on what a class decorator can do. A class decorator has to be on the top of the class, like previously we have seen decorator on method name or field name. This decorator is also a function but it should return a constructor function instead of a class.

Let’s say I have a simple User class like below.

class User {
constructor( firstName, lastName ) {
this.firstName = firstName;
this.lastName = lastName;
}
}

Our User class do not have any method at the moment. As discussed, a class decorator must return a constructor function.

function withLoginStatus( UserRef ) {
return function( firstName, lastName ) {
this.firstName = firstName;
this.lastName = lastName;
this.loggedIn = false;
}
}
@withLoginStatus
class User {
constructor( firstName, lastName ) {
this.firstName = firstName;
this.lastName = lastName;
}
}
let user = new User( 'John', 'Doe' );
console.log( user );

A class decorator function will receive the target class UserRef, which is User in the above example (on which decorator is applied) and must return a constructor function. This opens the door of infinite possibilities that you can do with the decorator. Hence class decorators are more popular than method/property decorators.

But the above example is too basic and we wouldn’t want to create a new constructor when our User class might have tons of properties and prototype methods. Good thing is, we have reference to the class from within the decorator function i.e. UserRef. We can return a new class from constructor function and that class will extends User class (more accurately UserRef class). Since the class is also a constructor function underneath, this is legal.

function withLoginStatus( UserRef ) {
return class extends UserRef {
constructor( ...args ) {
super( ...args );
this.isLoggedIn = false;
}
setLoggedIn() {
this.isLoggedIn = true;
}
}
}
@withLoginStatus
class User {
constructor( firstName, lastName ) {
this.firstName = firstName;
this.lastName = lastName;
}
}
let user = new User( 'John', 'Doe' );
console.log( 'Before ===> ', user );
// set logged in
user.setLoggedIn();
console.log( 'After ===> ', user );

You can chain multiple decorators together by placing them on top of each other. The order of execution will be the same as the order of their appearance.

Decorators are a fancy way to achieve things faster. Wait for some time until they are added to ECMAScript specifications.

--

--