/
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.
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).
The objective is to apply the decorator to the class and method:
@simpleDecorator
before the class C
. A message should
appear in the console window.
@simpleDecorator
before the m()
. Two messages should
appear in the console window, one for the method and one about decorating the class.
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.
The objective is to run the @log()
decorator with different
options:
@log ('warn')
before the class method sum()
. A
warning is displayed in the console.
log
or error
.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.
The objective is to see the difference between @init:log
and
@log
.
@init:log
before the class method sum()
. See the
console result.
init:
and keep @log
before the class method
sum()
. See the result and check the difference with the previous execution.
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.
The objective is to understand the decorator execution order.
@order( _ )
the applied position for each decorator,
from 1 to 6.
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.
The objective is to create a decorator myDecorator
that
writes in the console when a method is called.
function myDecorator(method) {}
.return function (...args) {}
.console.log('method called')
.return method.apply(this, args)
.sum()
method.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.
The objective is to create myLog
decorator. This decorator
receives a message and displays this in the console when the decorator is applied:
function myLog(message) {}
return function () {}
.
console.log(message)
.@log('the method "sum()" is deprecated')
to the
sum()
method.
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.
The objective is to add a new method into the class prototype:
sum
decorator and paste with the name min
.sum
with min
in all locations and change
the operator from +
to -
.
@min
to the class.console.log('c.min(1,2) =', c.min(1, 2))
.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.
The objective is to create a class decorator that adds two methods by inheriting from the original class:
function operations(element, context)
.context.kind
is different to "class"
.
return class extends element {}
.
mul(a,b) { return a * b }
and div(a,b) { return a / b }
.
@operations
.console.log
at the end of the code.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'
.
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:
function log(method, context) {}
.context.kind
is equal to "method"
.return function(...args) {}
.const return =
method.apply(this, args)
.
console.log
to write the method name, the parameters and the result.
return result
.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) {}
.
The objective is to adapt the previous exercise to getter/setter and apply it:
@log
to get value()
and set value(v)
.
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"
.
The objective is to create a decorator than put a random value into a field:
function random(value, context) {}
.return function(value) { return Math.random(); }
.
@random
to the field num
.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.
The objective is to convert to number any value assigned to the .num
property:
function number(value, context) {}
.context.kind
is "auto-accessor
.{ set(v) {}}
.set
method convert the parameter to number and call to the
original value.set.call(this, Number(v))
.
@number accessor
to the num
property.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
.
The objective is to create a log
decorator that displays a message when a
static method, getter, or setter is called.
function log(value, context) {}
'method'
, 'getter'
or 'setter'
.
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.
The objective is to complete the log decorator with a message, including if the member is private.
get()
and
@log
to #value
private auto accessor field.Some decorators need to be called with@init:
. This form allows the decorator
to add an initializer function. This initializer is executed:
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.
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.
function readonly(value, context) {}
.context.addInitializer
exist and throw an error if not.
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)
{}});
n
field.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.
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.
const IS_HIDDEN = Symbol()
.hide
decorator that writes metadata with this Symbol:
context.setMetadata(IS_HIDDEN, true)
.
context.getMetadata(IS_HIDDEN)
into
log
decorator and cancel the execution if the metadata is
true
.
@hide
before than @log
(remember the decorator
order: from top to bottom and left to right).
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.
Class[Symbol.metadata][KEY].constructor
Class.prototype[Symbol.metadata][KEY].public[member_name]
Class.prototype[Symbol.metadata][KEY].private
Class[Symbol.metadata][KEY].public[member_name]
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.
The objective is to check the metadata storage for each kind of element.
const MY_META = Symbol()
meta
that receive a message and write the message
as metadata.
@meta()
decorator for each element with a descriptive
parameter.
console.log(C[Symbol.metadata][MY_META])
and
console.log(C.prototype[Symbol.metadata][MY_META])
.
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::
const HISTORY = Symbol()
const data = new WeakMap();
@mutation
decorator to set the methods that change the status:
context.setMetadata (HISTORY, 'change');
@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)
@history
decorator with these features:
const access = value.prototype[ Symbol.metadata ][ HISTORY ].private[0];
history()
method to the prototype:
value.prototype.history = function () { if (!data.has(this)) { data.set(this, []) } return data.get(this) };
undo()
method to the prototype:
value.prototype.undo = function () { if (this.history().length) { access.set.call(this, this.history().pop () ); } };
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); }; });
@state
to #value = 0
.@mutation
to add()
, sub()
,
mul()
, and div()
.
@history
to class Calc
calc.undo()
to restore the previous status.