Tags: TypeScript
Decorators allow us to add additional information to classes or methods in TypeScript. They're similar to annotations such as in Java. With decorators we can add a wide variety of functionality, with a very succinct notation. It is planned that decorators will become a standard part of JavaScript, making it important to learn how they work.
Decorators are markers which can be attached to classes or methods. With them, we can attach information or functionality to classes, methods, parameters, properties, or accessors in JavaScript. They are easy to implement, but there are many details, and the documentation is sometimes weak.
The TypeScript documentation describes Decorators this way:
.... Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members. Decorators are a stage 2 proposal for JavaScript and are available as an experimental feature of TypeScript.
A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form
@expression
, whereexpression
must evaluate to a function that will be called at runtime with information about the decorated declaration.
While decorators are a stage 2 proposal, it seems the TypeScript implementation predates that status. In other words, in the due course of time this feature should become part of JavaScript, at which time TypeScript should change to match. Therefore, decorators in TypeScript are an experimental feature which must be explicitly enabled.
For a detailed description see:
- https://github.com/tc39/proposal-decorators -- In the due course of time it is expected that decorators will be a standard part of the ECMAScript language. This is the ECMAScript committee working on making that happen.
The TypeScript implementation is lagging well behind the proposal that's being worked on. This means we should expect, if/when the ECMAScript committee finishes its work, for the TypeScript implementation to change to match.
There is a related feature, the Reflection Metadata API, that is meant to work in concert with decorators. It will be an addition to the Reflection API, amplifying the ability to use Reflection to manipulate the data describing objects. This API is also in the development process.
Because decorators are on their way to becoming a standard part of JavaScript, it's well worth our time to understand what they do, and even how to implement them. They may look magical, but each decorator is implemented by a function, and with enough study of both the decorator and reflection API's we too could start implementing them.
For those of us who've spent time writing Java or C# code, we've probably had experience with Annotations. These decorators look very similar to Annotations, and serve roughly the same purpose.
This article is part of a series:
- Introduction to Decorators This article
- Class Decorators
- Property Decorators
- Accessor Decorators
- Parameter Decorators
- Method Decorators
- Hybrid Decorators
- Using Reflection and Reflection API with Decorators
- Runtime data validation using Decorators
Setting up a Node.js project for TypeScript decorators
Setting up a project directory simply requires installing the TypeScript compiler, and adding a tsconfig.json
file containing the compiler configuration directives.
$ npm init -y
$ npm install typescript @types/node --save-dev
$ npm install reflect-metadata --save
This initializes the directory with a package.json
and installs the minimal required files. The reflect-metadata
package is required for most decorators, and contains a polyfill implementing the Reflection Metadata API.
It appears there is a Babel plugin for compiling JavaScript code containing decorators. See: https://www.npmjs.com/package/@babel/plugin-proposal-decorators
Enabling experimental features in TypeScript for decorators
As we said, decorators are an experimental feature because it's thought the implementation will change if/when the feature becomes a standard part of JavaScript.
In your tsconfig.json
file make these settings:
{
"compilerOptions": {
...
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
...
}
}
The first, experimentalDecorators
, turns on decorator support.
The second, emitDecoratorMetadata
, emits data required by the reflect-metadata
package. This package enables us to do powerful things in decorators by recording metadata about classes, properties, methods, and parameters.
Companion repository on GitHub
The code shown in this article is also available on GitHub: https://github.com/robogeek/typescript-decorators-examples
Within this repository is a package,
decorator-inspectors
. Its decorators are useful for investigating or inspecting the things you're decorating.
The five types of decorators in TypeScripts
There are five decorator types:
- Class Decorators are attached to classes
- Property Decorators are attached to property definitions
- Accessor Decorators are attached to either the
get
orset
method associated with a property - Method Decorators are attached to methods
- Parameter Decorators are attached to method parameters
They look like this:
@ClassDecorator()
class A {
@PropertyDecorator()
name: string;
@MethodDecorator()
fly(
@ParameterDecorator()
meters: number
) {
// code
}
@AccessorDecorator()
get egg() {
// code
}
set egg(e) {
// code
}
}
In other words, it is the @
symbol, followed by a function name, and sometimes there are parameters to the function. It's not that the decorator name looks like a function name, it is a function name. The parentheses are optional, depending on the type of decorator and its function. Decorators can receive arguments, in which case the parentheses are required.
Every decorator is implemented by a function of the same name.
Finding decorator packages / libraries for TypeScript
While we can implement our own decorators, it's much simpler if someone has already created a package of decorators covering your needs.
Useful npm searches:
Selected decorator packages:
- class-logger - Logs class creation, and method invocation
- class-validator - Data validation decorators
- typescript-memoize - Record the value for method or accessor calls, to avoid recomputing expensive values
- class-transformer - Transformation between plain data and class instances
- fastify-decorators - Fastify
- catch-finally-decorator - Catch exceptions from methods
- lynx-framework - App framework built from several existing components, using decorators widely
- core-decorators - Several widely useful decorators - as well as a discussion of the actual state of decorator standardization
-
@loopback/metadata - Part of
@Loopback
, a library of decorators to help build decorators - routing-controllers - "Structured, declarative and beautifully organized class-based controllers with heavy decorators usage in Express / Koa using TypeScript and Routing Controllers Framework."
- validatorjs-decorator - ValidatorJS as decorators
- overnight - TypeScript decorators for Express
- @reflet/express - TypeScript decorators for Express
Frameworks
- TS.ED - Billed as a full-fledged app framework, with wide use of decorators
- TypeORM - Full featured ORM for TypeScript/JavaScript, with wide use of decorators
Developing your own decorators versus using existing decorator packages
In the previous section we noted many packages in the npm/yarn repository supply predeveloped packages for a wide variety of uses. Typically we will chose the already developed package, so we can get on with whatever we need to do. Hopefully the package author will have tested their code, spent some time designing it carefully, and so forth, and we can return to our application development.
But of course that's not always the case. Your needs might be different, or there might not be a package fulfilling your area of need.
The articles in this series focus on understanding how to develop decorators for use in TypeScript applications.
General pattern for defining and using decorators
While it's true that every decorator is implemented by a function, each decorator type requires a function with a specific signature. That is, for each decorator type, the decorator function receives a different set of parameters, and TypeScript carefully matches the decorator function with the required signature.
Immediately following the @
character there must be an expression that evaluates to a function, with the correct signature, that will be called at runtime. The invoked function will be given parameters giving information about the thing being decorated.
The stereotypical use is:
function DecoratorName(parameters) {
// decorator code
}
class ClassName {
@DecoratorName
methodName(methodParameters) {
// method code
}
}
That is, you define a function, and use the function name in the decorator. In this case the expression is simply the function name. So long as that function has the correct signature, this works great.
Another typical pattern is the decorator factory:
function OtherDecorator(parametersForDecorator) {
// preprocessing of parameters
return (decorator function parameters) => { // actual decorator function
// decorator code
}
}
class OtherClass {
@OtherDecorator(param1, param2)
methodName(methodParameters) {
// method code
}
}
In this case, the OtherDecorator
function returns a function with the correct signature to be a decorator. The expression in this case is the call, OtherDecorator(param1, param2)
, and the result of that expression is the interior function. For this to work, the interior function must implement the correct signature. This pattern is used in cases where additional data is required.
What happens is, the invocation of the outer function, OtherDirector(param1, param2), is an expression which returns a function with the correct signature to be a class decorator. That makes the inner function the decorator function, and the outer function a factory generating the decorator function.
When are parentheses required on a decorator? This has to do with the nature of each decorator definition. For a decorator function that has the correct signature, the parentheses cannot be used. For every decorator following the factory model, the parentheses are required, and must be used, in order for the function call to be evaluated, causing the inner function to be returned.
Again, the decorator is @
followed by an expression evaluating to a function with the correct signature. To make this even clear, consider defining a decorator in-line with the code being decorated:
@((constructor: Function) => {
console.log(`Inline constructor decorator `, constructor);
})
class InlineDecoratorExample {
// properties and methods
}
This is an @
character followed by an expression, namely an inline arrow function, which evaluates to a function having the correct signature. This example contains the correct signature for a class decorator.
It is difficult to come up with a rationale for using an in-line decorator. One purpose for decorators is code sharing, and implementing functionality using a concise notation. There is nothing about an in-line decorator that supports either goal, but it is allowed by the TypeScript compiler.
Evaluation order when there are multiple decorators
It's allowed to use more than one decorator:
@decorator1 @decorator2 @decorator4
@decorator3(param1, param2)
class MultiDecoratorClass {
// methods and properties
}
There can be more than one decorator per line of text, and you can have as many decorators as you like - within the memory or computation limits of your computer.
The rules for evaluating - the execution order - of multiple decorators are:
- The expressions for each decorator are evaluated top-to-bottom.
- The results are then called as functions from bottom-to-top.
The expression, "expression for each decorator", refers to what follows the @
character is an expression which evaluates to a decorator function. For the decorators which do not require parentheses, the decorator name directly maps to a decorator function, and therefore the expression is simply the function name. For decorator factories, the expression means to evaluate the invocation of the outer function, which returns the inner function.
That happens from top-to-bottom in the order they appear in the text.
The actual decorator functions are then executed from bottom-to-top.
Order of evaluation for each type of decorator
Evaluating the decorators attached to a class occurs in a prescribed order:
- Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each instance member - a.k.a. the normal methods.
- Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each static member - a.k.a. the static methods.
- Parameter Decorators are applied for the constructor.
- Class Decorators are applied for the class.
Can TypeScript decorators be used on standalone functions, types or interfaces?
Decorators can only be used within classes. Every decorator type is applied either to a class, or to a member of a class. To verify this, let's do a little experiment, to try decorators with things declared outside classes. Let's start with decorating a bare function:
function Decorator() {
console.log('In Decorator');
}
@Decorator
function decorated() {
console.log('in decorated');
}
Inside Visual Studio Code you'll get the answer - a red line under the @
and the message that decorators are not valid there. And, using the compiler:
$ npx tsc function.ts
function.ts:6:1 - error TS1206: Decorators are not valid here.
6 @Decorator
~
Found 1 error.
That's pretty clear, that a decorator cannot be used on a bare function. The error was not that the signature of the decorator is invalid, but simply that decorators are not valid in this location. Even if you use the correct signature for a method decorator, the compiler gives the same error message.
function foo(@logParameter x:number) {
// function
}
This is an attempt to use a parameter decorator on a parameter to a standalone function. And, again, we get the same error that decorators are not valid here.
@Decorator
interface XyzzyInterface {
x: number;
y: number;
zzy: number;
}
@Decorator
type XyzzyType = {
x: number;
y: number;
zzy: number;
};
You will see the same error message, that decorators are not allowed here, when attaching decorators to interface
or type
definitions.