Here's how decorators work in JavaScript (ES6, TypeScript, ...)

Here's how decorators work in JavaScript (ES6, TypeScript, ...)

Here's how to use decorators in JavaScript

Introduction

Decorators in general are not a fairly new concept, they've been in use in other languages such as python for quite some time, but they just came into the playfield with the Decorator ECMAScript proposal, which is currently in stage 3 as of this time of writing this article.

In this article, you should understand what decorators are, how to make use of them, and when it might be necessary to make use of them.

What are decorators?

Decorators are functions called on classes, class elements, or other JavaScript syntax forms during the definition. They are essentially higher-order functions,

  function double(Z){
   return function single(y){
      Return Z(Z(y));
  }
}

( a function that either takes another function as an argument and returns a value or a function that returns another function), which are written to extend the functionality of what is being decorated without fundamentally changing its behaviour.

According to the official ECMASCRIPT proposal for decorators Decorators have three primary capabilities:

  • They can replace the value that is being decorated with a matching value that has the same semantics. (e.g. a decorator can replace a method with another method, a field with another field, a class with another class, and so on).

  • They can provide access to the value that is being decorated via accessor functions which they can then choose to share.

  • They can initialize the value that is being decorated, running additional code after the value has been fully defined. In cases where the value is a member of class, then initialization occurs once per instance.

Essentially, decorators can be used to metaprogram and add functionality to a value, without fundamentally changing its external behavior.

How do they work?

Syntax

There is no special syntax for defining decorators, any function can be applied as a decorator.

A decorator expression begins with the @ and is followed by the name of the decorator. Decorators are called with () and could receive arguments. Arbitrary expressions could also be used as a decorator using the syntax @(expression). Class expressions could be decorated, not just class declarations. Decorators come after export and default .

// Decorator  being defined
function Decorator (value, context){
  return value.call();
}

// decorator expression being used
@Decorator 
randomFunction(){
  //Do something
}

//or

@Decorator("Some Value")
randomFunction(){
  //Do stuff here...
}

Decorators begin with the "@" symbol at the very start or top of the function they are trying to decorate. For example, @Decorator above is a decorator that has been declared for the method randomFunction(){}.

Decorators can be applied to four types of values:

  • Classes
  • Class Fields
  • Class Methods
  • Class accessors

When decorators are applied, they are evaluated in three steps:

  1. First the decorator expressions ( the thing after the @) are evaluated interspersed with computed property names. This action flows from the left to the right and from top to bottom. The result of this evaluation is stored in the equivalent of local variables to be later called after the class definition initially finishes executing.

  2. Decorators are called (as functions) during class definition, after the methods have been evaluated but before the constructor and prototype have been put together. For example @superUser decorator could be evaluated to the function

// @superUser evaluats to  ==> 
function superUser(){
  //do stuff here
}

.

When a decorator is called, it receives two parameters;

  1. The value being decorated or undefined when it's a class field being decorated
  2. A context object containing information about the value being decorated
// JavaScript takes an input and context object
const Decorator = (input, context) => {
  return output
}

// Using typescript interface for clarity
type Decorator = (value: Input, context: {
  kind: string;
  name: string | symbol;
  access: {
    get?(): unknown;
    set?(value: unknown): void;
  };
  private?: boolean;
  static?: boolean;
  addInitializer?(initializer: () => void): void;
}) => Output | void;

The values passed in the context object vary according to the type of value being decorated (classes, class methods, class fields, class accessors).

3.) Decorators are applied (mutating the constructor and prototype) all at once after all of them have been called. First, the newly constructed class is not made available until after all methods and non-static field decorators have been applied, then the class decorator is called only after all methods and field decorators are called and applied. Then finally, all static fields are executed and applied.

Decorators for Class Methods

type ClassMethodDecorator = (value: Function, context: {
  kind: "method";
  name: string | symbol;
  access: { get(): unknown };
  static: boolean;
  private: boolean;
  addInitializer(initializer: () => void): void;
}) => Function | void;

The class method decorator receives the method that is being decorated as its first value, and can optionally return a new method to replace it. If a new method is returned, it will replace the original on the prototype or on the class itself in the case of static methods.

For example:


// The @logged decorator receives the original function `m()` and returns 
// a new function that wraps the original and logs before and after the function is called

function logged(value, { kind, name }) {
  if (kind === "method") {
    return function (...args) {
      console.log(`starting ${name} with arguments ${args.join(", ")}`);
      const ret = value.call(this, ...args);
      console.log(`ending ${name}`);
      return ret;
    };
  }
}

class C {

  // Method decorator "logged"
  @logged
  m(arg) {}
}

new C().m(1);
// starting m with arguments 1
// ending m

Decorators for Class fields

type ClassFieldDecorator = (value: undefined, context: {
  kind: "field";
  name: string | symbol;
  access: { get(): unknown, set(value: unknown): void };
  static: boolean;
  private: boolean;
}) => (initialValue: unknown) => unknown | void;

Unlike the method decorator, the class field decorator doesn't have a direct input value when being decorated. Instead, users can optionally return an initializer function which runs when the field is assigned. This will receive the initial value of the field and return a new initial value. If any other type of value besides a function is returned, an error will be thrown.

function logged(value, { kind, name }) {
  if (kind === "field") {
    return function (initialValue) {
      console.log(`initializing ${name} with value ${initialValue}`);
      return initialValue;
    };
  }

  // ...
}

class C {
  @logged x = 1;
}

new C();
// initializing x with value 1

It's also worthy to note that since class fields already return an initializer , they do not receive addInitializer and cannot add additional initialization logic.

Decorators for Classes

type ClassDecorator = (value: Function, context: {
  kind: "class";
  name: string | undefined;
  addInitializer(initializer: () => void): void;
}) => Function | void;

Class decorators receives the class that is being decorated as a the first parameter and then returns a callable ( a function, a class or a Proxy) to replace the original class. If a non callable is returned an error will be thrown.

function logged(value, { kind, name }) {
  if (kind === "class") {
    return class extends value {
      constructor(...args) {
        super(...args);
        console.log(`constructing an instance of ${name} with arguments ${args. join(", ")}`);
      }
    }
  }

  // ...
}

@logged
class C {}

new C(1);
// constructing an instance of C with arguments 1

Here's how to use Decorators in your next project

JavaScript

Decorators haven't been natively added into JavaScript, and it's still in stage 3 of the 4 staged approval process. Therefore to use decorators in our project we need to set up a transpiler such as babel.

Here's a plugin for decorators in barbel babeljs.io/docs/en/babel-plugin-proposal-de..

Typescript

If you are using Typescript you first need to enable the experimental Decorators compiler option either on the command line or in your tsconfig.json to properly use decorators:

Command Line:

to --target ES5 --experimentalDecorators

tsconfig.json:

  "compiler options": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}