xc banner image

An Introduction to JavaScript Decorators

Senior FrontEnd Developer
  • Twitter
  • LinkedIn


If you've heard the term "decorators" or spotted them in code snippets but still aren’t sure what they do, this post is for you. Here, we’ll demystify decorators, explore what they are, and understand how they can be useful.

Property Descriptors

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

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

console.log(Object.getOwnPropertyDescriptor(foo, 'bar'));

This will return an object like this:

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

Now, let’s break down these properties:

  • configurable: Indicates whether the property descriptor can be modified.
  • enumerable: Indicates whether the property will show up in property loops such as for...in or Object.keys
  • value: The value assigned to the property.
  • writable: Indicates whether the value can be changed.

To update this descriptor, 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, we can move on to decorators.

Decorators

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

Example: Making a Method Readonly

Imagine a simple User firstName and lastName properties, along with a getName method:

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

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

A user could override the getName method:

const user = new User('John', 'Doe');
user.getName(); // => "John Doe"

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

To prevent this, we can define a readonly decorator to update the property descriptor for getName so it cannot be 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"

Here, the readonly decorator modifies the property descriptor by setting writable to false.

Example: Adding a Logger

Decorators can also intercept methods. For instance, let’s log every time a logIn method is called:

function logger(elementDescriptor) {
    const originalFunction = elementDescriptor.descriptor.value;
    elementDescriptor.descriptor.value = function (username, password) {
        originalFunction.call(this, 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)"

Key Takeaways

  1. Decorators can modify class properties, methods, or the class itself.
  2. They enable powerful functionalities such as adding metadata, enforcing rules, and extending behavior dynamically.
  3. Decorators build on property descriptors, providing an elegant syntax for advanced configurations.

Notes

Decorators are currently a stage 2 proposal in the TC39 process. To experiment with decorators, you’ll need to install the @babel/plugin-proposal-decorators package.