Deep introduction to using and implementing TypeScript decorators

; Date: Thu Feb 10 2022

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, where expression 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:

  • (github.com) 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:

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: (www.npmjs.com) 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: (github.com) https://github.com/robogeek/typescript-decorators-examples

Within this repository is a package, (www.npmjs.com) 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 or set 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:

Frameworks

  • (tsed.io) TS.ED - Billed as a full-fledged app framework, with wide use of decorators
  • (typeorm.io) 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:

  1. The expressions for each decorator are evaluated top-to-bottom.
  2. 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:

  1. Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each instance member - a.k.a. the normal methods.
  2. Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each static member - a.k.a. the static methods.
  3. Parameter Decorators are applied for the constructor.
  4. 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.

About the Author(s)

(davidherron.com) David Herron : David Herron is a writer and software engineer focusing on the wise use of technology. He is especially interested in clean energy technologies like solar power, wind power, and electric cars. David worked for nearly 30 years in Silicon Valley on software ranging from electronic mail systems, to video streaming, to the Java programming language, and has published several books on Node.js programming and electric vehicles.

Books by David Herron

(Sponsored)