An Introduction to JavaScript Decorators


If you’ve heard of the term “decorators”, or spotted them in code snippets, but still aren’t sure what they do - then this post is for you. In this post I’ll attempt to demystify what decorators are as well as how they can be useful.


Property Descriptors


Before diving into decorators, we must first understand what "property descriptors" are and how they work. In a nutshell - a property descriptor is an object which contains metadata regarding an object property. To view a property descriptor, you can use the Object.getOwnPropertyDescriptor method. For example:

var foo = {
    bar: 'Hello world',
};

Object.getOwnPropertyDescriptor(foo, 'bar');

… which will return an object that looks like this:

{
    configurable: true,
    enumerable: true,
    value: "Hello world",
    writable: true,
}

Now let’s break down what each of these properties represent:

  • configurable: indicates whether or not the property descriptor properties can be modified.
  • enumerable: indicates whether or not the object property will show up in property loops such as for … in, Object.keys, etc.
  • value: the value assigned to the object property.
  • writable: indicates whether or not the value can be modified.

If you want to update this object, you can use the Object.defineProperty method. For example, let’s make it so that users can’t update foo.bar:

var foo = {
    bar: 'Hello world',
};

Object.defineProperty(foo, 'bar', {
    writable: false,
});

foo.bar = 'testing testing 1 2 3';

console.log(foo.bar); // => "Hello world"

With this knowledge in mind, we can now move on to decorators.


Decorators


Decorators are prefixed with an @ symbol and are meant for class properties, methods - or the class itself. They allow you to modify (amongst other things) the property descriptor object. Let’s explore a couple of potential use cases for this.

Imagine you have a very simple User class with a firstName and lastName property, along with a getName method that concatenates and logs both properties:

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

    getName() {
        console.log(`${ this.firstName } ${ this.lastName }`);
    }
}

Theoretically, a user could override your getName method to be anything they like. For example:

const user = new User('John', 'Doe');

user.getName(); // => "John Doe"

user.getName = function () {
    console.log('George Washington');
}

user.getName(); // => "George Washington"

… as you can see, George Washington is returned now, even though the first and last name properties are still set to John and Doe. To prevent this from happening, we can define a readonly decorator to update the property descriptor object for getName - which will prevent it from being rewritten:

function readonly(elementDescriptor) {
    elementDescriptor.descriptor.writable = false;
    return elementDescriptor;
}

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

    @readonly
    getName() {
        console.log(`${ this.firstName } ${ this.lastName }`);
    }
}

const user = new User('John', 'Doe');

user.getName(); // => "John Doe"

user.getName = function () {
    console.log('George Washington');
}

user.getName(); // => "John Doe"

The elementDescriptor parameter you see above is detailed here. The elementDescriptor object contains a descriptor property - which is the same property descriptor object we described at the beginning of this blog post:

{
    configurable: true,
    enumerable: true,
    value: "Hello world",
    writable: true,
}

So by setting the writable property to false, we can prevent users from arbitrarily overriding the method with a new value.

In addition to modifying the property descriptor object, you can intercept methods entirely. For example, let’s say we want to console.log whenever a logIn method is used. We could replace readonly with a logger decorator like so:

function logger(elementDescriptor) {
    const originalFunction = elementDescriptor.descriptor.value;

    elementDescriptor.descriptor.value = function (username, password) {
        originalFunction(username, password);
        console.log(`${ elementDescriptor.key } called with ${ username } at ${ new Date() }`);
    }
}

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

    @logger
    logIn(username, password) {
        // login logic here
    }
}

const user = new User('John', 'Doe');

user.logIn('JohnDoe', 'abc123');

// => "logIn called with JohnDoe at Mon Sep 09 2019 13:06:21 GMT-0400 (Eastern Daylight Time)"

For the sake of the brevity I’ve only given examples that modify a class method, but remember that decorators can also be used to modify class properties or even the class itself.

With the ability to modify metadata or intercept a property entirely - the possibilities are almost endless.


Additional Notes


Decorators are still a stage 2 feature - so if you’re interested in giving decorators a spin, you'll need to npm install the @babel/plugin-proposal-decorators package.

Also, if you’re interested in tracking the progress of this feature through the TC39 process, you can do so here.

Categories: Front-End Development
Tags: JavaScript;

SEARCH ARTICLES