Decorators
TypeScript introduces decorators
feature, metadata expressions similar to Java annotation tags or C# and Swift attributes.
ECMAScript does not yet have native support for annotating classes and class members (the feature is in the proposal
state),
so decorators
is an experimental TypeScript feature.
Decorators have a traditional notation of @expression
where expression
is the name of the function that should be invoked at runtime.
This function receives decorated
target as a parameter and can be attached to:
- class declaration
- method
- accessor
- property
- parameter
Class Decorators
Class decorators are attached to class declarations. At runtime, the function that backs the decorator gets applied to the class constructor. That allows decorators inspecting, modifying or even replacing class instances if needed.
Here's a simple example of the LogClass
decorator that outputs some log information every time being invoked:
function LogClass(constructor: Function) {
console.log('LogClass decorator executed for the constructor:');
console.log(constructor);
}
Now you can use newly created decorator with different classes:
@LogClass
class TextWidget {
text: string;
constructor(text: string = 'default text') {
this.text = text;
}
render() {
console.log(`Rendering text: ${this.text}`);
}
}
When a new instance of TextWidget
class is created, the @LogClass
attribute will be automatically invoked:
let widget = new TextWidget();
widget.render();
The class decorator should produce the following output:
LogClass decorator executed for the constructor:
[Function: TextWidget]
Rendering text: default text
Decorators with parameters
It is also possible passing values to decorators. You can achieve this with a feature known as decorator factories
.
A decorator factory is a function returning an expression that is called at runtime:
Let's create another simple decorator with log output that accepts additional prefix
and suffix
settings:
function LogClassWithParams(prefix: string, suffix: string) {
return (constructor: Function) => {
console.log(`
${prefix}
LogClassWithParams decorator called for:
${constructor}
${suffix}
`);
};
}
It can now be tested with the TextWidget
class created earlier:
@LogClassWithParams('BEGIN:', ':END')
class TextWidget {
text: string;
constructor(text: string = 'default text') {
this.text = text;
}
render() {
console.log(`Rendering text: ${this.text}`);
}
}
let widget = new TextWidget();
widget.render();
You have marked TextWidget
class with the LogClassWithParams
decorator having a prefix
and suffix
properties
set to BEGIN:
and :END
values. The console output, in this case, should be:
BEGIN:
LogClassWithParams decorator called for:
function TextWidget(text) {
if (text === void 0) { text = 'default text'; }
this.text = text;
}
}
:END
Multiple decorators
You are not limited to a single decorator per class. TypeScript allows declaring as much class and member decorators as needed:
@LogClass
@LogClassWithParams('BEGIN:', ':END')
@LogClassWithParams('[', ']')
class TextWidget {
// ...
}
Note that decorators are called from right to left, or in this case from bottom to top. It means that first decorator that gets executed is:
@LogClassWithParams('[', ']')
and the last decorator is going to be
@LogClass
Method Decorators
Method decorators are attached to class methods and can be used to inspect, modify or completely replace method definition of the class. At runtime, these decorators receive following values as parameters: target instance, member name and member descriptor.
Let's create a decorator to inspect those parameters:
function LogMethod(target: any,
propertyKey: string,
descriptor: PropertyDescriptor) {
console.log(target);
console.log(propertyKey);
console.log(descriptor);
}
Below is an example of this decorator applied to a render
method of TextWidget
class:
class TextWidget {
text: string;
constructor(text: string = 'default text') {
this.text = text;
}
@LogMethod
render() {
console.log(`Rendering text: ${this.text}`);
}
}
let widget = new TextWidget();
widget.render();
The console output in this case will be as following:
TextWidget { render: [Function] }
render
{ value: [Function],
writable: true,
enumerable: true,
configurable: true }
Rendering text: default text
You can use decorator factories
also with method decorators to support additional parameters.
function LogMethodWithParams(message: string) {
return (target: any,
propertyKey: string,
descriptor: PropertyDescriptor) => {
console.log(`${propertyKey}: ${message}`);
};
}
This decorator can now be applied to methods. You can attach multiple decorators to a single method:
class TextWidget {
text: string;
constructor(text: string = 'default text') {
this.text = text;
}
@LogMethodWithParams('hello')
@LogMethodWithParams('world')
render() {
console.log(`Rendering text: ${this.text}`);
}
}
let widget = new TextWidget();
widget.render();
Note that decorators are called from right to left, or in this case from bottom to top. If you run the code the output should be as follows:
render: world
render: hello
Rendering text: default text
Accessor Decorators
Accessor decorators are attached to property getters
or setters
and can be used to inspect, modify or completely replace accessor definition of the property.
At runtime, these decorators receive following values as parameters: target instance, member name and member descriptor.
Note that you can attach accessor decorator to either getter
or setter
but not both.
This restriction exists because on the low level decorators deal with
Property Descriptors
that contain both get
and set
accessors.
Let's create a decorator to inspect parameters:
function LogAccessor(target: any,
propertyKey: string,
descriptor: PropertyDescriptor) {
console.log('LogAccessor decorator called');
console.log(target);
console.log(propertyKey);
console.log(descriptor);
}
Now the decorator can be applied to the following TextWidget
class:
class TextWidget {
private _text: string;
@LogAccessor
get text(): string {
return this._text;
}
set text(value: string) {
this._text = value;
}
constructor(text: string = 'default text') {
this._text = text;
}
}
let widget = new TextWidget();
Once invoked the decorator should produce the following output:
LogAccessor decorator called
TextWidget { text: [Getter/Setter] }
text
{ get: [Function: get],
set: [Function: set],
enumerable: true,
configurable: true }
Same as with class and method decorators you can use decorator factories feature to pass parameters to your accessor decorator.
function LogAccessorWithParams(message: string) {
return (target: any,
propertyKey: string,
descriptor: PropertyDescriptor) => {
console.log(`Message from decorator: ${message}`);
}
}
TypeScript allows using more than one decorator given you attach it to the same property accessor:
class TextWidget {
private _text: string;
@LogAccessorWithParams('hello')
@LogAccessorWithParams('world')
get text(): string {
return this._text;
}
set text(value: string) {
this._text = value;
}
constructor(text: string = 'default text') {
this._text = text;
}
}
let widget = new TextWidget();
The console output should be as shown below, note the right-to-left execution order:
Message from decorator: world
Message from decorator: hello
In case you declare decorator for both accessors TypeScript generates an error at compile time:
class TextWidget {
private _text: string;
@LogAccessorWithParams('hello')
get text(): string {
return this._text;
}
@LogAccessorWithParams('world')
set text(value: string) {
this._text = value;
}
}
error TS1207: Decorators cannot be applied to multiple get/set accessors of the same name.
Property Decorators
Property decorators are attached to class properties. At runtime, property decorator receives the following arguments:
- target object
- property name
Due to technical limitations, it is not currently possible observing or modifying property initializers. That is why property decorators do not get Property Descriptor value at runtime and can be used mainly to observe a property with a particular name has been defined for a class.
Here's a simple property decorator to display parameters it gets at runtime:
function LogProperty(target: any, propertyKey: string) {
console.log('LogProperty decorator called');
console.log(target);
console.log(propertyKey);
}
class TextWidget {
@LogProperty
id: string;
constructor(id: string) {
this.id = id;
}
render() {
// ...
}
}
let widget = new TextWidget('text1');
The output in this case should be as following:
LogProperty decorator called
TextWidget { render: [Function] }
id
Parameter Decorators
Parameter decorators are attached to function parameters. At runtime, every parameter decorator function is called with the following arguments:
- target
- parameter name
- parameter position index
Due to technical limitations, it is possible only detecting that a particular parameter has been declared on a function.
Let's inspect runtime arguments with this simple parameter decorator:
function LogParameter(target: any,
parameterName: string,
parameterIndex: number) {
console.log('LogParameter decorator called');
console.log(target);
console.log(parameterName);
console.log(parameterIndex);
}
You can now use this decorator with a class constructor and method parameters:
class TextWidget {
render(@LogParameter positionX: number,
@LogParameter positionY: number) {
// ...
}
}
Parameter decorators are also executed in right-to-left order.
So you should see console outputs for positionY
and then positionX
:
LogParameter decorator called
TextWidget { render: [Function] }
render
1
LogParameter decorator called
TextWidget { render: [Function] }
render
0