Introduction
JavaScript decorators are still in an experimental stage, called "draft" or "stage 2", which means that it could take some time before they are integrated into the JavaScript standard. However, TypeScript has been supporting them for some time now, but only in an experimental way. Decorators have become very popular in frameworks such as Angular and Vue, where they are used for dependency injection and adding functions to a class.
Since decorators are not yet standardized, it is important to note that their implementation and standards in JavaScript and TypeScript could change at any time. This means that decorator code we write today could require many modifications if the specifications were to change. This chapter aims to introduce and understand decorators, especially for those who use them heavily in frameworks.
Decorators are a feature of the TypeScript compiler and are supported in ES5 and later versions. To use them, we need to enable a compilation option in the tsconfig.json file. This option is called "experimentalDecorators" and must be set to "true", as follows:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
By adding this option to our configuration file, we enable the use of decorators in our TypeScript code. We can then start experimenting with decorators and adding features to our classes and objects.
Syntaxe
A decorator is a function that is called with a specific set of parameters. These parameters are automatically filled by the JavaScript runtime and contain information about the class, method, or property that the decorator was applied to. The number and types of these parameters determine where a decorator can be applied. To illustrate this syntax, let's take the example of a class decorator:
function simpleDecorator(constructor: Function) {
console.log('simpleDecorator called');
}
This function can be used as a class decorator and applied to a class definition, as follows:
@simpleDecorator
class ClassWithSimpleDecorator {}
The execution of this code will produce the following output:
simpleDecorator called
In this example, we haven't created an instance of the class named ClassWithSimpleDecorator yet. All we've done is specify its definition, add a decorator to it, and it was automatically called by the JavaScript runtime environment. This shows that decorators are applied when a class is defined.
Let's prove this theory by creating a few instances of this class:
let instance_1 = new ClassWithSimpleDecorator();
let instance_2 = new ClassWithSimpleDecorator();
console.log(`instance_1 : ${JSON.stringify(instance_1)}`);
console.log(`instance_2 : ${JSON.stringify(instance_2)}`);
result:
simpleDecorator called
instance_1 : {}
instance_2 : {}
The above code demonstrates how decorators can be used to add additional features to a class in TypeScript. When we apply a decorator to a class, the decorator function is called with the class as a parameter.
In this example, we used a simple decorator called "simpleDecorator" to add a feature to our "ClassWithSimpleDecorator" class. We created two instances of this class, but the decorator function was only called once.
It's also possible to apply multiple decorators on the same class. In the above example, we added a second decorator called "secondDecorator" to our "ClassWithMultipleDecorators" class. Both decorators were then called, but in the reverse order of their appearance in the code. So, the second decorator was called first, followed by the first decorator.
function secondDecorator(constructor: Function) {
console.log(`secondDecorator called`);
}
@simpleDecorator
@secondDecorator
class ClassWithMultipleDecorators {
}
Résultat:
secondDecorator called
simpleDecorator called
It is important to note that the order in which decorators are applied can have an impact on the behavior of the class. Therefore, it's important to understand how decorators work and to use them with care.
Types
Decorators in TypeScript can be applied to different parts of the code, depending on the type of decorator used. There are four types of decorators: class decorators, property decorators, method decorators, and parameter decorators.
Class decorators can be applied to a class definition. Property decorators can be applied to a property inside a class. Method decorators can be applied to a method on a class. Finally, parameter decorators can be applied to a parameter of a method inside a class.
To illustrate these different types of decorators, let's look at the following example:
function classDecorator(
constructor: Function) {}
function propertyDecorator(
target: any,
propertyKey: string) {}
function methodDecorator(
target: any,
methodName: string,
descriptor?: PropertyDescriptor) {}
function parameterDecorator(
target: any,
methodName: string,
parameterIndex: number) {}
-
The first function, "classDecorator", can be used as a class decorator. It has a single parameter named "constructor" of type "Function".
-
The second function, "propertyDecorator", can be used as a property decorator. It has two parameters: the first parameter is named "target" and is of type "any", and the second parameter is named "propertyKey" and is of type "string".
-
The third function, "methodDecorator", can be used as a method decorator. It has three parameters: the first parameter, named "target", is of type "any", and the second parameter is named "methodName" and is of type "string". The third parameter is an optional parameter named "descriptor" and is of type "PropertyDescriptor".
-
The fourth function, "parameterDecorator", can be used as a parameter decorator. It also has three parameters: the first parameter is named "target" and is of type "any". The second parameter is named "methodName" and is of type "string". The third parameter is named "parameterIndex" and is of type "number".
@classDecorator
class ClassWithAllTypesOfDecorators {
@propertyDecorator
id: number = 1;
@methodDecorator
print() { }
setId(@parameterDecorator id: number) { }
}
Indeed, what is important to remember about decorators in TypeScript is that the number of parameters and their types determine whether they can be used as class, property, method or parameter decorators.
When a decorator is applied to a class, property, method, or parameter, it will be called by the JavaScript runtime at runtime. The JavaScript runtime will automatically provide the appropriate parameters based on the type of decorator applied.
It is also important to note that decorators are not limited to a single type of parameter. A decorator can be used to decorate both a class, property, method, and parameter, depending on how it is defined. This allows for great flexibility in how decorators can be used to add functionality to TypeScript code.
Decorator factories
It may happen that we need to define a decorator that has parameters. To achieve this, we need to use what is called a decorator factory function. A decorator factory function is created by wrapping the decorator function itself in another function.
In the following example, we define a decorator factory function that takes a parameter named "name". The factory function then returns an anonymous decorator function that logs a message to the console that includes the "name" parameter.
function decoratorFactory(name: string) {
return (constructor: Function) => {
console.log(`decorator function called with : ${name}`);
};
}
It may happen that we need to define a decorator that has parameters. To achieve this, we need to use what is called a "decorator factory" function. A decorator factory function is created by wrapping the decorator function itself in another function.
In the following example, we define a decorator factory function that takes a parameter named "name". The factory function then returns an anonymous decorator function that logs a message to the console that includes the "name" parameter.
typescript
Copy code
function decoratorFactory(name: string) {
return (constructor: Function) => {
console.log(decorator function called with: ${name}
);
};
}
We can now use this decorator factory function to decorate our "ClassWithDecoratorFactory" class as follows:
@decoratorFactory('testName')
class ClassWithDecoratorFactory {
}
The result will be a message logged to the console that includes the "name" parameter that we passed to the decorator factory function.
It is important to note that decorator factory functions must return a function that has the right number and types of parameters depending on what type of decorator they are. Additionally, parameters defined for the decorator factory function can be used anywhere in the function definition, including in the anonymous decorator function itself.
Class Decorators
Class decorators are a key concept in object-oriented programming that allow modifying the behavior of classes and their instances. Decorators are functions that take the class as a parameter and can add additional functionality to the class.
A common example of a class decorator is the @classConstructorDec function. This function is called on the ClassWithConstructor class, which is then used to create an instance of the class. The code looks like this:
function classConstructorDec(constructor: Function) {
console.log(`constructor : ${constructor}`);
}
@classConstructorDec
class ClassWithConstructor {
constructor(id: number) { }
}
Dans cet exemple, la fonction @classConstructorDec
prend le constructeur de la classe ClassWithConstructor
en tant que paramètre et l'affiche dans la console. Le constructeur de la classe est simplement une fonction qui prend un paramètre "id" et ne fait rien d'autre. Cependant, la fonction @classConstructorDec
peut être utilisée pour ajouter des propriétés ou des méthodes supplémentaires à la classe.
Un autre exemple de décorateur de classe montre comment ajouter une propriété à l'objet prototype de la classe. Le code ressemble à ceci :
function classConstructorDec(constructor: Function) {
console.log(`constructor : ${constructor}`);
constructor.prototype.testProperty = "testProperty_value";
}
In this example, the function @classConstructorDec adds a "testProperty" property to the prototype object of the class. This property has the value "testProperty_value" and is automatically available for all instances of the class.
The last code example shows how to access the "testProperty" property of an instance of the ClassWithConstructor class. The code looks like this:$
let classInstance = new ClassWithConstructor(1);
console.log(`classInstance.testProperty =
${(<any>classInstance).testProperty}`);
$
In this example, an instance of the ClassWithConstructor class is created using the class constructor. Then, the "testProperty" property is accessed using dot syntax. Since the property was added to the prototype object of the class, it is automatically available for all instances of the class.
Property Decorator
function propertyDec(target: any, propertyName: string) {
console.log(`target : ${target}`);
console.log(`target.constructor : ${target.constructor}`);
console.log(`propertyName : ${propertyName}`);
}
Property decorators in TypeScript can be used to add additional functionality to the properties of a class. In the example above, we have created a property decorator called "propertyDec" and applied it to the "nameProperty" property of the "ClassWithPropertyDec" class:
class ClassWithPropertyDec {
@propertyDec
nameProperty: string | undefined;
}
When we apply a property decorator to a class property, the decorator function is called with two parameters. The first parameter is the object of the class definition itself, and the second parameter is the name of the decorated property.
In our example, the "propertyDec" decorator function was called with the class definition object of "ClassWithPropertyDec" and the name of the decorated property "nameProperty". In the decorator function, we printed to the console the value of the "target" object, which is the class definition object itself, the value of the "constructor" property of the "target" object, and the name of the "propertyName".
When we ran our code with the decorator applied to the "nameProperty" property, we got the following output in the console:
target : [object Object]
target.constructor : function ClassWithPropertyDec() {}
propertyName : nameProperty
We can see that the first parameter "target" is an object that represents the class definition itself. The second line of the console output shows that this object has a "constructor" property, which is the constructor function of the "ClassWithPropertyDec" class. Finally, the last line of the console output shows the name of the property that we decorated.
Property decorators can be used to add additional functionality to the properties of a class, but it is important to understand their impact on the code before using them.
Static Property Decorators
Property decorators can also be applied to static class properties in the same way as for normal properties. However, the arguments passed to the decorator function will be slightly different.
class StaticClassWithPropertyDec {
@propertyDec
static staticProperty: string;
}
résultat
target : function StaticClassWithPropertyDec() {}
target.constructor : function Function() { [native code] }
propertyName : staticProperty
When we apply a property decorator to a static class property, the first argument passed to the decorator function will be a function representing the class definition itself. The "constructor" property of this function will be the constructor function of the class. The second argument will be the name of the decorated property.
function propertyDec(target: any, propertyName: string) {
if (typeof (target) === 'function') {
console.log(`class name : ${target.name}`);
} else {
console.log(`class name : `
+ `${target.constructor.name}`);
}
console.log(`propertyName : ${propertyName}`);
}
In the example above, we applied the same "propertyDec" decorator to a normal property and a static class property. In the decorator function, we added a check to see if the first argument is a function, and in that case, we printed the name of the class using the "name" property of the function. Otherwise, we printed the name of the class using the "name" property of the "constructor" property of the object passed as the first argument.
When we ran our code with the decorator applied to both a normal property and a static property, we got the following output in the console:
class name : ClassWithPropertyDec
propertyName : nameProperty
class name : StaticClassWithPropertyDec
propertyName : staticProperty
We can see that we are able to determine the name of the class and the name of the decorated property, whether the property was marked static or not. It is important to understand that property decorators can have an impact on the behavior of the class or the application, so it is important to use them with care.
Method Decorators
Method decorators are functions that can be used to add additional behavior to a class method in JavaScript or TypeScript. They have three parameters: the target object, the name of the method, and an optional descriptor object that describes the method. Method decorators can be used to add logging, validation, error handling, and other features to a method.
Here is an example of a method decorator that logs messages to the console every time a method is called:
function auditLogDec(target: any, methodName: string, descriptor?: PropertyDescriptor) {
let originalFunction = target[methodName];
let auditFunction = function (this: any) {
console.log(`auditLogDec: Overriding ${methodName} called`);
for (let i = 0; i < arguments.length; i++) {
console.log(`auditLogDec: arg ${i} = ${arguments[i]}`);
}
originalFunction.apply(this, arguments);
}
target[methodName] = auditFunction;
return target;
}
class MyClass {
@auditLogDec
myMethod(arg1: string, arg2: number) {
console.log(`myMethod: ${arg1}, ${arg2}`);
}
}
let obj = new MyClass();
obj.myMethod("test", 123);
In this example, the auditLogDec decorator is applied to the myMethod method of the MyClass class. When myMethod is called, the decorator logs messages to the console before and after the original method call. This allows us to track the execution flow of the method and debug any potential errors more easily.
In summary, method decorators are a powerful tool for adding additional functionality to a class method in JavaScript or TypeScript. They can be used to add logging, validation, error handling, and other features to a method.
Parameter Decorators
Parameter decorators can be useful for adding additional functionality to a method based on the value of the parameter.
When a parameter decorator is used, the JavaScript runtime provides the decorator with three arguments:
-The target object on which the method is defined. -The name of the method that contains the decorated parameter. -The index of the parameter in the list of parameters for the method. For example, here is an example of a parameter decorator:
function parameterDec(target: any, methodName: string, parameterIndex: number) {
console.log(`target: ${target}`);
console.log(`methodName : ${methodName}`);
console.log(`parameterIndex : ${parameterIndex}`);
}
class ClassWithParamDec {
print(@parameterDec value: string) {
}
}
In this example, we have defined a parameter decorator named parameterDec that logs the information provided by the JavaScript runtime about the decorated parameter. We then applied this decorator to a parameter of the print method of the ClassWithParamDec class.
When we run this code, the JavaScript runtime logs the following information to the console:
target: [object Object]
methodName : print
parameterIndex : 0
We can see that the JavaScript runtime provided the decorator with information about the decorated method and parameter.
Parameter decorators can be useful for adding additional functionality to a method based on the value of the parameter. For example, we could use a parameter decorator to validate the inputs to the method or to add annotations to the method documentation based on the parameters.
Decorator Metadata
Parameter decorators are a useful feature in TypeScript for adding functionality to a method or class by defining specific decorators. However, when we use decorators, we don't have all the information about the parameters we are decorating.
This is where decorator metadata comes in. Decorator metadata is additional information that can be carried along when using a decorator. It provides us with more information about the method or class we are decorating.
To enable this feature, we need to set the "emitDecoratorMetadata" flag in our "tsconfig.json" file to "true". Here is an example of a "tsconfig.json" file configuration to enable decorator metadata:
{
"compilerOptions": {
// other compiler options
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
An example of using decorator metadata in TypeScript is adding additional functionality to an existing class or method. In the following example, we define a method decorator called "log" that logs information about the method call and the returned result:
import "reflect-metadata";
function log(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with arguments: ${args.join(", ")}`);
const result = originalMethod.apply(this, args);
console.log(`Result: ${result}`);
return result;
};
}
class Calculator {
@log
public add(x: number, y: number): number {
return x + y;
}
}
const calculator = new Calculator();
const result = calculator.add(1, 2);
console.log(`Final result: ${result}`);
In this example, we apply the "log" decorator to the "add" method of the "Calculator" class. When the "add" method is called, the "log" decorator is executed and the recorded information is displayed in the console.
Decorator metadata can also be used to write frameworks for dependency injection or to generate code analysis tools. However, it is important to note that the type information used by our code and the TypeScript compiler is compiled into the resulting JavaScript. Therefore, the use of decorator metadata should be used with caution to avoid slowing down the performance of the application.
Conclusion
We have explored the use of decorators in TypeScript and how they can add additional functionality to classes, properties, methods, and class parameters. Each decorator has its own set of required parameters and parameter types, depending on where it needs to be used. Decorators are a powerful tool for adding additional functionality to our TypeScript code, and decorator metadata provides additional information about our classes at runtime. However, it is important to use them with caution to avoid slowing down the performance of the application.