Javascript Proposal Decorators Tutorial

|  Playground |  Quick Reference
version

Tutorial

/

Introduction

This tutorial describes step by step how to use and build the new Javascript decorators. The decorators provide a defined way to add features or metadata to class declarations and class members.

Javascript decorators specification are in stage 2 of standardized process and is subject to change before incorporating it into the Javascript standard. You can also review the proposal documentation for more information.

This interactive tutorial will help you get started with Javascript decorators and see how easy it is to use and build these new elements. Each step of this tutorial introduces one or two decorator features.

Use the interactive editor to add features to the initial code sample and see the result in the console. If you have problems with a step, click the "resolve" button to see the finished code, or click the "reset" button to return to the initial code.

Use a decorator

Let's discover how to use a decorator in your classes. Remember that decorators provide a way to add functionality to your classes and class members. We will use very simple decorators in this tutorial. Real-life decorators usually give much more value.

Usually, the decorator is obtained with an import expression. Decorators are functions and can be imported without problems from modules. Of course, they can also be written the decorator and the class in the same script, especially while learning how they work.

The first key point is that the decorator is applied with @ before the decorator's name and writing this statement before the decorated element. The decorator can be written on the same line as the decorated element or on the lines before it.

A decorator can be attached to a class or a class member (methods, getter/setter, fields, public, private, or static).

Exercise

The objective is to apply the decorator to the class and method:

  • Write @simpleDecorator before the class C. A message should appear in the console window.
  • Write @simpleDecorator before the m(). Two messages should appear in the console window, one for the method and one about decorating the class.

Use a decorator with parameters

Some decorators need explicit configurations for their work. In these cases, the decorator is called as a function with parenthesis, and the parameters are included between them.

You have to read the decorator's documentation to know the accepted and necessary parameters for each decorator.

Exercise

The objective is to run the @log() decorator with different options:

  • Write @log ('warn') before the class method sum(). A warning is displayed in the console.
  • Change the decorator parameter to log or error.

Use decorator with @init

In some cases, the decorator requires to be invoked with @init:. This clause is necessary when the decorator needs to include an initializer function to rightly apply the decorator.

You have to read the decorator's documentation to know what is the correct way to call it.

Exercise

The objective is to see the difference between @init:log and @log.

  • Write @init:log before the class method sum(). See the console result.
  • Remove init: and keep @log before the class method sum(). See the result and check the difference with the previous execution.

Decorator order

If a class or a class member has several decorators, these are called from left to right and from top to bottom.

The member class decorators are called before the class decorators.

The static member class decorators are called after the class decorators.

Exercise

The objective is to understand the decorator execution order.

  • Include into the @order( _ ) the applied position for each decorator, from 1 to 6.

Build a simple decorator

Build your own decorator is very simple. A decorator is a function that receives two parameters, the first is the element decorated, and the second is an object with information and helpers (we will see this context object in the next steps in this tutorial).

The return of our decorator can replace the element decorated. For example, if it returns a new function, this function replaces the method.

Exercise

The objective is to create a decorator myDecorator that writes in the console when a method is called.

  • Create a function with function myDecorator(method) {}.
  • This first function return a new function: return function (...args) {}.
  • Inside the returned function write a console.log('method called').
  • Inside the returned function write a call to the original method: return method.apply(this, args).
  • Apply the decorator over the sum() method.

Build a decorator with parameters

If your decorator needs to receive configuration options, you must create a function that receives this information as parameters and returns the decorator function. You need two nested functions, the first function gets the configuration and the second function is the decorator.

Exercise

The objective is to create myLog decorator. This decorator receives a message and displays this in the console when the decorator is applied:

  • Create a function with function myLog(message) {}
  • In this function return the decorator function: return function () {}.
  • Inside the returned function write a console.log(message).
  • Add the decorator @log('the method "sum()" is deprecated') to the sum() method.

Class decorator

Decorators can be applied to the class and to the class members. We need a way to identify the type of element on which our decorator is applied. The decorator's second parameter is the solution. This context has a property kind that contains the kind of element our decorator is operating.

A class decorator can check the correct element with context.kind === 'class' and cancel de execution if the type is other. Normally, decorators do not throw an error when applied to an element for which they are not prepared and ignore the case.

The decorator receives the class and can check or modify the class content, such as adding a new method into the prototype or checking if a method is implemented.

Exercise

The objective is to add a new method into the class prototype:

  • Copy the sum decorator and paste with the name min.
  • Change sum with min in all locations and change the operator from + to -.
  • Add @min to the class.
  • Check the result with console.log('c.min(1,2) =', c.min(1, 2)).

A class decorator can replace the class

When the decorated element is a class, we can optionally return a new class and replace the original one. We must be careful replacing the class because we can break the code that uses the class that expects a specific behavior or signature. A possible solution is to create a new class that inherits from the original.

Exercise

The objective is to create a class decorator that adds two methods by inheriting from the original class:

  • Create the decorator with function operations(element, context).
  • Cancel de execution if context.kind is different to "class".
  • Return a new class that inherits from the original class with return class extends element {}.
  • Inside this inherited class add two methods: mul(a,b) { return a * b } and div(a,b) { return a / b }.
  • Apply the decorator on the class with @operations.
  • Check the result with two console.log at the end of the code.

Method decorator

These decorators receive the method, check its characteristics, and optionally return a new function to replacing the original method.

The method decorators can check the kind element with context.kind === 'method'.

Exercise

The objective is to write a @log that wraps the original method and writes in the console the method's name (included in the context.name), the parameters, and the return value when the method is called:

  • Create the decorator with function log(method, context) {}.
  • Check if context.kind is equal to "method".
  • Return a new function with return function(...args) {}.
  • In this function, call to the original method with const return = method.apply(this, args).
  • Use console.log to write the method name, the parameters and the result.
  • Include in this function return result.
  • Apply this decorator to the methods.

Getter/Setter decorator

A decorator applied to a getter or setter is very similar to a method decorator. You can identify this kind of decorator with the values "getter" and "setter" into context.kind.

These decorators apply only for a getter or a setter. If you need to decorate both, you can put a decorator in each function, i.e., you must write a decorator for the getter @log get value() {}, and a decorator for the setter @log set value(v) {}.

Exercise

The objective is to adapt the previous exercise to getter/setter and apply it:

  • Change the decorator for accept getter and setters.
  • Apply @log to get value() and set value(v).

Field decorator

The field decorator is a little different because the first parameter received by the decorator function is always undefined. As a result, we cannot operate directly on the field. To get the initial value and optionally change it, we must return a function that receives the initial value and can return the new initial value for the field.

We can identify the field decorator with context.kind with the value "field".

Exercise

The objective is to create a decorator than put a random value into a field:

  • Create the decorator function random(value, context) {}.
  • Check if the decorated element is or not a field.
  • Return a function than return a random value return function(value) { return Math.random(); }.
  • Apply the decorator @random to the field num.

Auto accessor

The new accessor keyword before a class field defines internally a getter and setter for this field and storage the value into a private slot. This kind of element can be decorated, and the decorator receives as value an object with the getter and setter ({get, set}) and the context.kind is "auto-accessor".

Optionally the auto-accessor decorator can return an object with a new getter and/or a new setter ({get, set}) that replace the originals. In addition, auto-accessors decorators can add an initialize method used to change the initial value.

Exercise

The objective is to convert to number any value assigned to the .num property:

  • Create the decorator function number(value, context) {}.
  • Check if context.kind is "auto-accessor.
  • Return a new object with a { set(v) {}}.
  • In the set method convert the parameter to number and call to the original value.set.call(this, Number(v)).
  • Apply @number accessor to the num property.

Static member decorator

The static class members (methods, fields, getter, setter, and auto accessors) can be decorated without problems and work in the same form as instance members. If you need to identify a static element, you can check if is true the property context.isStatic.

Exercise

The objective is to create a log decorator that displays a message when a static method, getter, or setter is called.

  • Create the decorator function log(value, context) {}
  • Check if the decorated element kind is 'method', 'getter' or 'setter'.
  • Return a wrapped function for the original element.
  • In this wrapped function, call to the original element.
  • Write in the console a message with information about the method, includes if it is static.

Private member decorator

It's straightforward to identify a private member decorator: the property context.isPrivate is true. When a decorator is applied to a private member, the decorator has some limitations because it cannot use context.name to access the member. The name included in this property is only for informational proposal. For access to the private member, we must use context.access and its methods get() and set(v).

The rest of the functionalities are precisely the same for each type of decorator, and we can use everything we have learned so far in this tutorial.

Exercise

The objective is to complete the log decorator with a message, including if the member is private.

  • Update the get() and set() methods for writing a message when the decorator is applied to a private member.
  • Apply @log to #value private auto accessor field.

@init: member decorator

Some decorators need to be called with@init:. This form allows the decorator to add an initializer function. This initializer is executed:

  • for member decorator, after the constructor (for each object)
  • for static member decorators, after the class is defined
  • for the class decorator, after the static member are defined.

The decorator can check if it is called or not with #init: checked the function context.addInitializer. If exist it, the decorator is called with @init:. This function context.addInitializer is the form to add new initializer functions.

Exercise

The objective is to create a decorator with @init: that defines a field as read-only. It's essential to understand that the member initializer runs after the constructor.

  • Create a decorator function readonly(value, context) {}.
  • Check if the context.addInitializer exist and throw an error if not.
  • Call to context.addInitializer() and pass as parameter a function that define the property as readonly with Object.defineProperty(this, context.name, { get() { return value; }, set(v) {}});
  • Apply the decorator to n field.

Metadata

Decorators provide a simple way to add metadata to classes and their members. Some people use "annotation" as a synonym of "decorator" because one of their features is to annotate classes and class members with metadata.

Metadata can be used by other decorators in a collaborative environment, can be used by other programs to recognize some features, can be used for debugging purposes, etc.

We need a Symbol key for write metadata. This is the key to avoiding name collisions between different metadata sources. With this Symbol is used when we call to context.setMetadata(key, value) to add metadata and context.getMetadata(key) to get metadata of current decorated element.

Exercise

The objective is to create the close decorator that adds metadata, and the log decorator reads the metadata and avoids logging if the previous metadata is defined.

  • Define a new Symbol with const IS_HIDDEN = Symbol().
  • Create the hide decorator that writes metadata with this Symbol: context.setMetadata(IS_HIDDEN, true).
  • Check the metadata with context.getMetadata(IS_HIDDEN) into log decorator and cancel the execution if the metadata is true.
  • Add @hide before than @log (remember the decorator order: from top to bottom and left to right).

Getting metadata

If you need to read the metadata outside the decorators of the same element, we need to find this metadata. The metadata is stored in the class with a particular structure, and we can access it if we have the original Symbol (KEY).

Symbol.metadata is a well know Symbol used for storing the metadata. Depending on whether the metadata has been defined, the class or the class element, public, private or static, the metadata is stored in different places.

  • The class metadata is located in
    Class[Symbol.metadata][KEY].constructor
  • The public member metadata is located in
    Class.prototype[Symbol.metadata][KEY].public[member_name]
  • The private member metadata is located in
    Class.prototype[Symbol.metadata][KEY].private
  • The static member metadata is located in
    Class[Symbol.metadata][KEY].public[member_name]
  • The static and private member metadata is located in
    Class[Symbol.metadata][KEY].private

Notice: The private metadata doesn't have the member's private name and is an array with all private metadata.

Exercise

The objective is to check the metadata storage for each kind of element.

  • Define a Symbol: const MY_META = Symbol()
  • Create a new decorator meta that receive a message and write the message as metadata.
  • Add the @meta() decorator for each element with a descriptive parameter.
  • Get all metadata with console.log(C[Symbol.metadata][MY_META]) and console.log(C.prototype[Symbol.metadata][MY_META]).

Final exercise

This complete exercise has the objective of creating a set of decorators to obtain the object's state, create a history, and add an undo() method to restore previous states.

In this case, we have to follow several steps::

  • Create a Symbol as the key for our metadata:
    const HISTORY = Symbol()
  • Create a WeakMap for storage the status for each object:
    const data    = new WeakMap();
  • Create a @mutation decorator to set the methods that change the status:
    context.setMetadata (HISTORY, 'change');
  • Create a @state accessor decorator to set the private field that stores the current status and put the access object into the metadata with
    context.setMetadata (HISTORY, context.access)
  • Create an @history decorator with these features:
    • Get the access object saved into the metadata:
      const access = value.prototype[ Symbol.metadata ][ HISTORY ].private[0];
    • Add the history() method to the prototype:
      value.prototype.history = function () {
        if (!data.has(this)) {
          data.set(this, [])
        }
        return data.get(this)
      };
    • Add the undo() method to the prototype:
      value.prototype.undo = function () {
        if (this.history().length) {
          access.set.call(this, this.history().pop () );
        }
      };
    • Wrap all methods annotated as a mutation with a new function that stores the previous state:
      Object.keys (value.prototype[ Symbol.metadata ][ HISTORY ].public).forEach (method => {
        const fn = value.prototype[ method ];
        value.prototype[ method ] = function (...args) {
          this.history().push (access.get.call(this));
          return fn.apply (this, args);
        };
      });
  • Apply the decorators:
    • @state to #value = 0.
    • @mutation to add(), sub(), mul(), and div().
    • @history to class Calc
  • Now, you can call calc.undo() to restore the previous status.
resolve
reset
:
:
:

Source code

....

Console output