Decorators are not part of the ECMAScript 6 specification, but they were proposed to the ECMAScript 7 standard for 2016. They provide us with a way to decorate classes and properties during design time. This allows a developer to use meta-annotations while writing classes, and declaratively attach functionality to the class and its properties.
Decorators are named after the decorator pattern that was initially described in the book Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma and his colleagues, also known as the Gang of Four (GoF).
The principle of decoration is that an existing procedure is intercepted and the decorator has the chance to either delegate, provide an alternative procedure, or do a mix of both:
Visualization of decoration in a dynamic environment with the example of a simple access procedure
Decorators in ECMAScript 7 can be used to annotate classes and class properties. Note that this also includes class methods, as class methods are also properties of the class prototype object. Decorators get defined as regular functions, and they can be attached to classes or class properties with the at symbol. Our decorator function will then be called with contextual information about the location of inclusion every time that the decorator is placed.
Let's take a look at a simple example that illustrates the use of a decorator:
function logAccess(obj, prop, descriptor) {
const delegate = descriptor.value;
descriptor.value = function() {
console.log(`${prop} was called!`);
return delegate.apply(this, arguments);
};
}
class MoneySafe {
@logAccess
openSafe() {
this.open = true;
}
}
const safe = new MoneySafe();
safe.openSafe(); // openSafe was called!
We have created a logAccess decorator that will log all function calls that are tagged with the decorator. If we look at the MoneySafe class, you can see that we have decorated the openSafe method with our logAccess decorator.
The logAccess decorator function will be executed for each annotated property within our code. This enables us to intercept the property definition of the given property. Let's take a look at the signature of our decorator function. Decorator functions that are placed on class properties will be called with the target object of the property definition as a first parameter. The second parameter is the actual property name that is defined, followed by the last parameter, which is the descriptor object that is supposed to be applied to the object.
The decorator gives us the opportunity to intercept the property definition. In our case, we use this ability to exchange the descriptor value (which is the annotated function) with a proxy function that will log the function call before calling the origin function (delegation). For simplification purposes, we've implemented a very simple yet incomplete function proxy. For real-world scenarios, it would be advisable to use a better proxy implementation, such as the ECMAScript 6 proxy object.
Decorators are a great feature to leverage aspect-oriented concepts and declaratively add behavior to our code at design time.
Let's look at a second example where we use an alternative way to declare and use decorators. We can treat decorators like function expressions where our decorator function is rewritten as a factory function. This form of usage is especially useful when you need to pass along configuration to the decorator, which is made available in the decorator factory function:
function delay(time) {
return function(obj, prop, descriptor) {
const delegate = descriptor.value;
descriptor.value = function() {
const context = this;
const args = arguments;
return new Promise(function(success) {
setTimeout(function() {
success(delegate.apply(context, arguments));
}, time);
});
};
};
}
class Doer {
@delay(1000)
doItLater() {
console.log('I did it!');
}
}
const doer = new Doer();
doer.doItLater(); // I did it! (after 1 second)
We have now learned how ECMAScript 7 decorators can help you write declarative code that has an aspect-oriented twist to it. This simplifies development a lot because we can now think of behavior that we add to our classes during design time when we actually think about the class as a whole and write the initial stub of the class.
Decorators in TypeScript are slightly different than the decorators from ECMAScript 7. They are not limited to classes and class properties, but they can be placed on parameters within the class methods. This allows you to annotate function parameters, which can be useful in some cases:
class TypeScriptClass {
constructor(@ParameterDecorator() param) {}
}
Angular uses this feature to simplify dependency injection on class constructors. As all directive, component, and service classes get instantiated from Angular dependency injection and not by us directly, these annotations help Angular find the correct dependencies. For this use case, function parameter decorators actually make a lot of sense.
Currently, there are still issues with the implementation of decorators on class method parameters, which is also why ECMAScript 7 does not support it. The TypeScript compiler has worked around this issue but is currently not compliant to the ECMAScript 7 proposal.