Decorators in TypeScript provide a way of programmatically tapping into the process of defining a class.
Remember that a class definition describes the shape of a class, what properties it has, and what methods it defines. When an instance of a class is created, these properties and methods become available on the class instance.
Decorators, however, allow us to inject code into the actual definition of a class, before a class instance has been created. They are similar to attributes in C#, or annotations in Java.
This article is an excerpt from the book, Mastering Typescript, Fourth Edition by Nathan Rozentals – a comprehensive guide for readers to build enterprise-ready, modular web applications using Typescript 4 and modern frameworks.
Decorator overview
In this section, we will take a look at the general setup and syntax of decorators, what you need to do to enable them, and how they are applied to classes. We will also show how multiple decorators can be used at the same time, and then discuss the different types of decorators.
Finally, we’ll take a look at decorator factories, and how we can pass parameters into decorator functions.
But before diving into our article, don’t miss the video The perfect cocktail: Java + Typescript, where open-sourcer Manuel Carrasco Moñino – with a long history contributions in projects such as Vaadin, Polymer, GWT or Apache – demonstrates how to develop a 100% type-safe enterprise app, including the back-end, the front-end, and even the data in the wire.
Decorator setup
Decorators are an experimental feature of the TypeScript compiler and are supported in ES5 and above. In order to use decorators, you need to enable a compile option in the tsconfig.json file. This option is named experimentalDecorators, and needs to be set to true, as follows:
{
“compilerOptions”: {
“target”: “es5”,
“module”: “commonjs”,
“strict”: true,
“experimentalDecorators”: true,
“skipLibCheck”: true,
“forceConsistentCasingInFileNames”: true
}
}
Here, we have set the compiler option named experimentalDecorators to true. This will allow the use of decorators within our TypeScript code.
Decorator syntax
A decorator is a function that is called with a specific set of parameters. These parameters are automatically populated by the JavaScript runtime, and contain information about the class, method, or property to which the decorator has been applied.
The number of parameters, and their types, determine where a decorator can be applied. To illustrate this syntax, let’s define a class decorator, as follows:
function simpleDecorator(constructor: Function) {
console.log(‘simpleDecorator called’);
}
Here, we have a function named simpleDecorator, which has a single parameter named constructor of the function type, which logs a message to the console, indicating that it has been invoked.
This function, due to the parameters that it defines, can be used as a class decorator function and can be applied to a class definition, as follows:
@simpleDecorator
class ClassWithSimpleDecorator {
}
Here, we have a class named ClassWithSimpleDecorator that has the simpleDecorator decorator applied to it. We apply a decorator using the “at” symbol (@), followed by the name of the decorator function. Running this code will produce the following output:
simpleDecorator called
Here, we can see that the simpleDecorator function has been invoked. What is interesting about this code sample, however, is that we have not created an instance of the class named ClassWithSimpleDecorator as yet.
All that we have done is specify the class definition, added a decorator to it, and the decorator has been called by the JavaScript runtime automatically.
Not having to wait for the creation of an instance of a class tells us that decorators are applied when a class is defined. Let’s prove this theory by creating a few instances of this class, as follows:
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)}`);
Here, we have created two new instances of ClassWithSimpleDecorator, named instance_1 and instance_2. We then log a message to the console to output the value of each class instance. The output of this code is as follows:
simpleDecorator called
instance_1 : {}
instance_2 : {}
Here, we can see that the simpleDecorator function has only been called once, even though we have created two instances of the ClassWithSimpleDecorator class.
Decorators are only invoked once, when a class is defined.
Multiple decorators
Multiple decorators can be applied one after another on the same target. As an example of this, let’s define a second decorator function as follows:
function secondDecorator(constructor: Function) {
console.log(`secondDecorator called`);
}
Here, we have a decorator function named secondDecorator, which also logs a message to the console once it has been invoked. We can now apply both simpleDecorator (from our earlier code snippet) and secondDecorator as follows:
@simpleDecorator
@secondDecorator
class ClassWithMultipleDecorators {
}
Here, we have applied both decorators to a class named ClassWithMultipleDecorators. The output of this code is as follows:
secondDecorator called
simpleDecorator called
Here, we can see that both of the decorators have logged a message to the console. What is interesting, however, is the order in which they are called.
Decorators are called in the reverse order of their appearance within our code.
Types of decorators
Decorators, as mentioned earlier, are functions that are invoked by the JavaScript runtime when a class is defined. Depending on what type of decorator is used, these decorator functions will be invoked with different arguments. Let’s take a quick look at the types of decorators, which are:
- Class decorators – These are decorators that can be applied to a class definition.
- Property decorators – These are decorators that can be applied to a property within a class.
- Method decorators – These are decorators that can be applied to a method on a class.
- Parameter decorators – These are decorators that can be applied to a parameter of a method within a class.
As an example of these types of decorators, consider the following code:
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) {}
Here, we have four functions, each with slightly different parameters.
- The first function, named classDecorator, has a single parameter named constructor of the function type. This function can be used as a class decorator.
- The second function, named propertyDecorator, has two parameters. The first parameter is named target, and is of the any type. The second parameter is named propertyKey and is of the string type. This function can be used as a property decorator.
- The third function, named methodDecorator, has three parameters. The first parameter, named target, is of the any type, and the second parameter is named methodName, and is of the string type. The third parameter is an optional parameter named descriptor, and is of the PropertyDescriptor type. This function can be used as a method decorator.
- The fourth function is named parameterDecorator, and also has three parameters. The first parameter is named target, and is of the any type. The second parameter is named methodName, and is of the string type. The third parameter is named parameterIndex, and is of the number type. This function can be used as a parameter decorator.
Let’s now take a look at how we would use each of these decorators as follows:
@classDecorator
class ClassWithAllTypesOfDecorators {
@propertyDecorator
id: number = 1;
@methodDecorator
print() { }
setId(@parameterDecorator id: number) { }
}
Here, we have a class named ClassWithAllTypesOfDecorators. This class has an id property of the number type, a print method, and a setId method. The class itself has been decorated by our classDecorator, and the id property has been decorated by the propertyDecorator.
The print method has been decorated by the methodDecorator function, and the id parameter of the setId function has been decorated by the parameterDecorator.
What is important to note about decorators is that it is the number of parameters and their types that distinguish whether they can be used as class, property, method, or parameter decorators. Again, the JavaScript runtime will fill in each of these parameters at runtime.
Decorator factories
On occasion, you will need to define a decorator that has parameters. In order to achieve this, we will need to use what is known as a decorator factory function. A decorator factory function is created by wrapping the decorator function itself within a function, as follows:
function decoratorFactory(name: string) {
return (constructor: Function) => {
console.log(`decorator function called with : ${name}`);
}
}
Here, we have a function named decoratorFactory that accepts a single parameter named name of the string type. Within this function, we return an anonymous function that has a single parameter named constructor of the function type.
This anonymous function is our decorator function itself, and will be called by the JavaScript runtime with a single argument.
Within the decorator function, we are logging a message to the console that includes the name parameter passed in to the decoratorFactory function. You can now use this decorator factory as follows:
@decoratorFactory(‘testName’)
class ClassWithDecoratorFactory {
}
Here we have applied the decorator named decoratorFactory to a class named ClassWithDecoratorFactory, and supplied the string value of “testName” as the name argument. The output of this code is as follows:
decorator function called with : testName
Here, we can see that the anonymous function returned by the decoratorFactory function was invoked with the string “testName” as the value of the name argument.
There are two things to note regarding decorator factory functions.
- Firstly, they must return a function that has the correct number of parameters, and types of parameters, depending on what type of decorator they are.
- Secondly, the parameters defined for the decorator factory function can be used anywhere within the function definition, which includes within the anonymous decorator function itself.
This concludes our discussion of the setup and use of decorators.
Summary
In this article, we have explored the use of decorators within TypeScript. We started by setting up our environment to make use of decorators, and then discussed the syntax used to apply decorators.
You can apply decorators to classes, class properties, class methods, and even class parameters.