JavaScript decorators from scratch

Future students of the course “JavaScript Developer. Professional” we invite you to sign up for an open lesson on the topic “Making an interactive telegram bot on Node.js”

And now we are sharing the traditional translation of useful material.


Understanding decorator functions

What is a decorator?

A decorator is a facility that allows you to wrap one function in another and extend its capabilities. You “decorate” existing code by wrapping it in other code. This trick is familiar to anyone familiar with function composition or higher-order functions.

Decorators are not new. They are used in other languages ​​as well, such as Python, and even functional programming in JavaScript. But we’ll talk about this later.

Why do we need decorators?

They allow you to write cleaner code, adhere to the concept of composition, and extend a once-developed capability to multiple functions and classes. By using decorators, you can write code that is easier to debug and maintain.

Decorators make the code of the main function compact because all the code for extending it is written outside of it. Decorators allow you to add new features to your code without complicating it.

The proposal to the standard for class decorators is currently at the 2nd stage of consideration, and many useful additions may still be added to this proposal.

Council. Share reusable components for different projects on the platform Bit (Github). It’s an easy way to document, organize and share independent components from any project.

The platform offers great opportunities for code reuse, collaboration on independent components, and development of scalable applications.

Bit supports Node, TypeScript, React, Vue, Angular and other JS frameworks.

Examples of reusable React components on Bit.dev
Examples of reusable React components on Bit.dev

Function decorators

What are function decorators?

Function decorators are just like functions. They take a function as an argument and return another function that extends the behavior of the argument function. The new function does not change the argument function, but uses it in its body. As I said, this is a lot like higher-order functions.

How do function decorators work?

Let’s look at an example.

Checking arguments is common programming practice. In languages ​​like Java, if a function expects two arguments and receives three, an exception is thrown. But there will be no error in JavaScript, since the extra parameters are simply ignored. This behavior of functions is sometimes annoying, but it can also be useful.

In order to make sure that the arguments are valid, you need to check them at the input. This is a simple operation that verifies that each parameter has the correct data type and does not exceed the number expected by the function.

However, repeating the same operation for several functions can lead to code repetition, so it is better to write a decorator to validate the arguments, which can then be reused with any functions.

//decorator function
const allArgsValid = function(fn) {
  return function(...args) {
  if (args.length != fn.length) {
      throw new Error('Only submit required number of params');
    }
    const validArgs = args.filter(arg => Number.isInteger(arg));
    if (validArgs.length < fn.length) {
      throw new TypeError('Argument cannot be a non-integer');
    }
    return fn(...args);
  }
}

//ordinary multiply function
let multiply = function(a,b){
	return a*b;
}

//decorated multiply function that only accepts the required number of params and only integers
multiply = allArgsValid(multiply);

multiply(6, 8);
//48

multiply(6, 8, 7);
//Error: Only submit required number of params

multiply(3, null);
//TypeError: Argument cannot be a non-integer

multiply('',4);
//TypeError: Argument cannot be a non-integer

In this example, we use the decorator function allArgsValidwhich takes a function as an argument. The decorator returns another function that wraps the argument function. In this case, the argument function is called only when the arguments passed to it are integers. Otherwise, an error is generated. The decorator also checks the number of parameters passed: it must strictly match the number that the function expects.

Then we declare a variable multiply and as a value we assign it a function that multiplies two numbers. We pass this multiplication function to the decorator function allArgsValidwhich, as we already know, returns another function. The returned function is assigned to the variable again multiply… Thus, the developed functionality can be easily reused.

//ordinary add function
let add = function(a,b){
	return a+b;
}

//decorated add function that only accepts the required number of params and only integers
add = allArgsValid(add);

add(6, 8);
//14

add(3, null);
//TypeError: Argument cannot be a non-integer

add('',4);
//TypeError: Argument cannot be a non-integer

Class decorators: proposal for a standard under consideration by TC39

Functional programming in JavaScript has been using function decorators for a long time. A proposal for class decorators is in the 2nd stage of consideration.

Classes in JavaScript are not really classes. Class syntax is just syntactic sugar for prototypes to make it easier to work with.

The conclusion is that classes are just functions. Then why don’t we use function decorators in classes? Let’s try.

Let’s look at an example of how this approach can be implemented.

function log(fn) {
  return function() {
    console.log("Execution of " + fn.name);
    console.time("fn");
    let val = fn();
    console.timeEnd("fn");
    return val;
  }
}

class Book {
  constructor(name, ISBN) {
    this.name = name;
    this.ISBN = ISBN;
  }

  getBook() {
    return `[${this.name}][${this.ISBN}]`;
  }
}

let obj = new Book("HP", "1245-533552");
let getBook = log(obj.getBook);
console.log(getBook());
//TypeError: Cannot read property 'name' of undefined

The error occurs because when calling the method getBook the anonymous function returned by the decorator function is actually called log… Inside the anonymous function, the method is called obj.getBook… But the key word this inside an anonymous function refers to a global object, not an object Book… An error occurs TypeError

This can be fixed by passing an instance of the object Book in method getBook

function log(classObj, fn) {
  return function() {
    console.log("Execution of " + fn.name);
    console.time("fn");
    let val = fn.call(classObj);
    console.timeEnd("fn");
    return val;
  }
}

class Book {
  constructor(name, ISBN) {
    this.name = name;
    this.ISBN = ISBN;
  }

  getBook() {
    return `[${this.name}][${this.ISBN}]`;
  }
}

let obj = new Book("HP", "1245-533552");
let getBook = log(obj, obj.getBook);
console.log(getBook());
//[HP][1245-533552]

We also need to pass an object Book into a decorator function logso that you can then pass it to the method obj.getBookusing this

This solution works great, but we had to work around it. The new proposal has optimized the syntax to make it easier to implement such solutions.

Note. To run the code from the examples below, you can use Babel. JSFiddle Is a simpler alternative that allows you to try out these examples in a browser. The proposals have not yet reached the last stage of consideration, so it is not recommended to use them in production: their functioning is not perfect yet and the syntax may be changed.

Class decorators

The new decorators use special syntax with the @ prefix. To call a decorator function log we will use the following syntax:

@log

In the proposal, some changes were made to the decorator functions compared to the standard. When a decorator function is applied to a class, it only receives one argument. This is the argument targetwhich is essentially an object of the class being decorated.

Having access to the argument target, you can make the changes you want to the class. You can change the constructor of a class, add new prototypes, and so on.

Let’s consider an example that uses the class Book, we already know him.

function log(target) {
  return function(...args) {
    console.log("Constructor called");
    return new target(...args);
  };
}

@log
class Book {
  constructor(name, ISBN) {
    this.name = name;
    this.ISBN = ISBN;
  }

  getBook() {
    return `[${this.name}][${this.ISBN}]`;
  }
}

let obj = new Book("HP", "1245-533552");
//Constructor Called
console.log(obj.getBook());
//HP][1245-533552]

As you can see, the decorator log gets an argument target and returns an anonymous function. She follows the instruction logand then creates and returns a new instance targetwhich is the class Book… Can be added to target prototypes using target.prototype.property.

Moreover, several decorator functions can be used with a class, as shown in this example:

function logWithParams(...params) {
  return function(target) {
    return function(...args) {
      console.table(params);
      return new target(...args);
    }
  }
}

@log
@logWithParams('param1', 'param2')
class Book {
	//Class implementation as before
}

let obj = new Book("HP", "1245-533552");
//Constructor called
//Params will be consoled as a table
console.log(obj.getBook());
//[HP][1245-533552]

Class property decorators

Their syntax, like the syntax for class decorators, uses the prefix @… You can pass parameters to class property decorators in the same way as in other decorators that we have seen with examples.

Class method decorators

The arguments passed to the class method decorator will be different from the class decorator arguments. The class method decorator receives not one, but three parameters:

  • target – an object that contains the constructor and methods declared inside the class;

  • name – the name of the method for which the decorator is called;

  • descriptor – the descriptor object corresponding to the method for which the decorator is called. You can read more about property descriptors. here

Most of the manipulation will be done with the descriptor argument. When used with a class method, a descriptor object has 4 attributes:

  • configurable – a boolean value that determines whether the properties of the descriptor can be changed;

  • enumerable – a boolean value that determines whether the property will be visible when enumerating object properties;

  • value – property value. In our case, this is a function;

  • writable – a boolean value that determines whether the property can be overwritten.

Consider an example with the class Book

//readonly decorator function
function readOnly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

class Book {
  //Implementation here
  @readOnly
  getBook() {
    return `[${this.name}][${this.ISBN}]`;
  }

}

let obj = new Book("HP", "1245-533552");

obj.getBook = "Hello";

console.log(obj.getBook());
//[HP][1245-533552]

It uses a decorator function readOnlywhich makes the method getBook in the class Book read-only. To this end, for the descriptor property writable the value is set false… By default it is set to true

If the value writable do not change, property getBook can be overwritten, for example, like this:

obj.getBook = "Hello";
console.log(obj.getBook);
//Hello

Class field decorators

Decorators can be used with class fields as well. Although TypeScript supports class fields, the proposal to add them to JavaScript is currently in Phase 3.

A decorator function used with a class field is passed the same arguments that are passed when using a decorator with a class method. The only difference is in the descriptor object. Unlike using decorators with class methods, when used with class fields, the descriptor object does not contain the attribute value… Instead, the function is used as an attribute initializer… Since the proposal to add class fields is still under consideration, about the function initializer can be read in documentation… Function initializer will return the initial value of the class field variable.

If the field is not assigned a value (undefined), attribute writable the descriptor object will not be used.

Let’s look at an example. We will work with the class already familiar to us Book

function upperCase(target, name, descriptor) {
  if (descriptor.initializer && descriptor.initializer()) {
    let val = descriptor.initializer();
    descriptor.initializer = function() {
      return val.toUpperCase();
    }
  }

}

class Book {
  
  @upperCase
  id = "az092b";

  getId() {
    return `${this.id}`;
  }

  //other implementation here
}

let obj = new Book("HP", "1245-533552");

console.log(obj.getId());
//AZ092B

This example converts the value of the id property to uppercase. Decorator function upperCase checks for function initializerto ensure that the value of the field is assigned a value (that is, the value is not undefined). It then checks to see if the assigned value is “conditionally true” (trans. Truthy – a value that turns into true when cast to type Boolean), and then converts it to uppercase. When calling the method getId the value will be displayed in uppercase. When using decorators with class fields, you can pass parameters in the same way as in other cases.

Use cases

The use cases for decorators are endless. Let’s see how programmers implement them in real applications.

Decorators in Angular

If you are familiar with TypeScript and Angular, you have probably come across decorators in Angular classes like @Component, @NgModule, @Injectable, @Pipe and so on. These are built-in class decorators.

MobX

Decorators in MobX were widely used up until version 6. Among them @observable, @computed and @action… But the use of decorators is currently discouraged in MobX because the proposal for the standard has yet to be adopted. The documentation says:

“Decorators are currently not an ES standard and the standardization process is long. Most likely, the use of decorators provided by the standard will differ from the current one. “

Core Decorators Library

It is a JavaScript library that contains ready-to-use decorators. Although it is based on the Stage 0 decorator proposal, the author plans to update when the proposal moves to Stage 3.

The library has decorators like @readonly, @time, @deprecate etc. Other decorators can be found here

Redux library for React

The Redux library for React has a method connect, with which you can connect your React component to the Redux store. The library allows you to use the method connect also as a decorator.

//Before decorator
class MyApp extends React.Component {
  // ...define your main app here
}
export default connect(mapStateToProps, mapDispatchToProps)(MyApp);
//After decorator
@connect(mapStateToProps, mapDispatchToProps)
export default class MyApp extends React.Component {
  // ...define your main app here
}

Felix Kling’s answer on Stack Overflow contains some explanations

Although connect supports decorator syntax, currently the Redux team discourages its use. This is mainly due to the fact that the proposal for decorators is at the 2nd stage of consideration, which means that changes may be made to it.

Decorators are a powerful tool that allows you to write very flexible code. Surely you will often encounter them in the near future.

Thanks for reading, and a clean code!


Learn more about the course “JavaScript Developer. Professional”.

Sign up for an open lesson on the topic “Making an interactive telegram bot on Node.js”.

Similar Posts

Leave a Reply