Javascript Decorators

What are Decorators in Javascript?

  • Decorators wrap a function in another function. These wrappers decorate the original function with new capabilities.

types of decorators in JavaScript

  • Function decorators — Wraps a function with another function.
  • Class decorators — Applied to the whole class at once.
  • Class member decorators — Applied to members of a class.

Function Decorators

  • In function decorators, we can simply wrap a JavaScript function with another function and use it as a decorator.
  • Let’s consider an example to understand this process.
  • The below code shows a simple JavaScript function that multiplies 2 numbers.
function multiply(x, y) {
  console.log('Total : ' + (x*y));
}
  • Now, we can wrap this function with another function to extend the functionalities without changing the original function.
function multiply(x, y) {
  console.log('Total : ' + (x*y));
}
function logDecorator(logger) {
  return function (message) {
    const result = logger.apply(this, arguments);
    console.log("Logged at:", new Date().toLocaleString());
    return result;
  }
}
const wrapperFunction = logDecorator(multiply);
wrapperFunction(10,10)
// output:
// Total: 100
// Logged at: 02/04/2022, 18:14:25
  • In the above example, the decorated function is in the variable wrapperFunction() and it can be called similar to any JavaScript function.
  • As you can see, the wrapperFunction() has modified the multiply() function by including a logger.

Class Decorators

  • Decorating JavaScript classes is a bit different from decorating functions.
  • If we try to use the same method as discussed in the function decorators section, we will get a TypeError like below:
function logDecorator (logger) {
  return function () {
    console.log("Logged at:", new Date().toLocaleString());
    return logger();
  }
}

class Calculator {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  multiply() {
    return (this.x * this.y);
  }
}

let calculator = new Calculator(10, 10);
let decoratedCalculator = logDecorator(calculator.multiply); 

decoratedCalculator ();
  • we can overcome this issue if you have a good understanding of JavaScript this keyword.
  • But that is not the easiest way to decorate a JavaScript class.
  • Instead, you need to follow the TC39 class decorator proposal to decorate JavaScript classes.

Class member decorators

  • Class member decorators are applied for single members in a class.
  • These members can be properties, methods, getters, or setters, and the decorator function accepts 3 input parameters:
  • target - The class that the member is on.
  • name - The name of the member in the class.
  • descriptor - The member descriptor.

  • For example, let’s consider the @readonly decorator.

function readonly(target, name, descriptor) {
  descriptor.writable = false;
   return descriptor;
}
class Example {
  x() {}
  @readonly
  y() {}
}
const myClass = new Example();
myClass.x = 10;
myClass.y = 20;
  • In the readonly() function, we have set the writable property of the descriptor to false.
  • Then, it is applied as a decorator to function y().
  • If you try to modify it, you will get a TypeError.
  • Also, we can create custom decorators and use them as class member decorators like below:
function log(name) {
  return function decorator(t, n, descriptor) {
    const original = descriptor.value;
    if (typeof original === 'function') {
      descriptor.value = function (...args) {
        console.log("Logged at:", new Date().toLocaleString());
        try {
          const result = original.apply(this, args);
          console.log(`Result from ${name}: ${result}`);
          return result;
        } cach (e) {
          console.log(`Error from $ {name}: ${e}`);
          thro e;
        }
      };
    }
    return descriptor;
  };
}
class Calculator {
  @log('Multiply')
    multiply(x,y){
      return x*y;
  }
}    
calculator = new Calculator();
calculator.multiply(10,10);
// output:
// Logged at: 1/12/2022, 08:00:00 PM
// Result from Multiply: 100
  • In this example, the Calculator class has a method named multiply(), and it is decorated using the log() function.
  • The log() function accepts a single parameter as input, and we can pass values into that when we call the decorator (@log('Multiply')).

Class decorators

  • Class decorators are applied to the complete class at once. So, any modification we make will impact the whole class.
  • Most importantly, anything you do with class decorators needs to end by returning a new constructor to replace the class constructor.
  • Class decorator functions only accept 1 argument, which is the constructor function being decorated.
  • Let’s take a simple example to understand this behavior.
function log(name) {
  return function decorator(Class) {
    return (...args) => {
      console.log(`Arguments for ${name}: ${args}`);
      return new Class(...args);
    };
  }
}

@log('Multiply')
class Calculator {
    constructor (x,y) { }
}  
calculator = new Calculator(10,10);
// Arguments for Multiply: [10, 10]
console.log(calculator);
// Calculator {}
  • The log() function of the above example accepts the Calculator class as an argument and returns a new function to replace the constructor of the Calculator class.

Advantages using decorators

  • The main purpose of introducing decorators to JavaScript is to share functionalities between JavaScript classes and class properties.
  • But, that’s not the only advantage decorators bring.
  • Decorators allow developers to write clean and reusable code.
  • Developers can easily separate the functional enhancements from the code features using decorators.
  • Apart from that, the decorator syntax is pretty simple and allows to add new features to classes and properties without increasing the code complexity. This makes the code easier to maintain and debug.

References: