7. Decorators

Overview

This chapter first establishes the motivation for decorators and then describes the various decorator types available in TypeScript. We'll take a look at how decorators are used and how they are customized to fit your specific needs. We'll also cover writing your own decorators. By the end of this chapter, you will be able to use decorators to alter the behavior of your code, and use decorator factories to customize the decorators that are being used. You will also learn how to create your own decorators, to be used by your code or that of others.

Introduction

In the previous chapters, you saw how to create types and classes and how to compose them into a proper class hierarchy using interfaces, inheritance, and composition.

Using the TypeScript type system, you can create some very elegant models of the domains of your applications. However, models do not live by themselves; they are part of a larger picture – they are part of an application. And classes need to be aware that they live in a larger world, with many other parts of the system running in tandem with them, with concerns that go beyond the scope of a given class.

Adding behaviors to or modifying classes to account for the preceding scenario is not always easy. And this is where decorators come to the rescue. Decorators are special declarations that can be added to class declarations, methods, and parameters.

In this chapter, we'll learn how you can use a technique called decorators to transparently add complicated and common behaviors to your classes, without getting your application logic all cluttered up with additional code.

Decorators are one of the features that are available and widely used in TypeScript but are not available in JavaScript. There is a proposal for decorators in JavaScript (https://github.com/tc39/proposal-decorators), but it's still not part of the standard. The decorators that you will use in TypeScript are closely modeled to function just like the proposal.

The TypeScript approach has its good and bad aspects. One good aspect is that once decorators become a standard feature in JavaScript, you can seamlessly transfer your decorating skill over to JavaScript, and the code that the TypeScript compiler (tsc) generates will be an even more idiomatic JavaScript. The bad thing is that until it becomes a standard feature, the proposal can and will change. That's why, by default, the usage of decorators is turned off in the compiler, and in order to use them, you need to pass in a flag, either as a command-line option or as part of your tsconfig.json. However, before you get into the details of how to do this, you first need to understand the concept of reflection, which will be explored in the following section.

Reflection

The concept of decorating your code is tightly coupled with a concept called reflection. In a nutshell, reflection is the capability of a certain piece of code to examine and be introspective about itself – in a sense, to do some navel-gazing. It means that a piece of code can have access to things such as the variables, functions, and classes defined inside it. Most languages provide us with some kind of reflection API that enables us to treat the code itself as if it was data, and since TypeScript is built upon JavaScript, it inherits the JavaScript reflection capabilities.

JavaScript does not have an extensive reflection API, but there is a proposal (https://tc39.es/ecma262/#sec-reflection) to add proper metadata (data about data) support to the language.

Setting Up Compiler Options

TypeScript's decorators use the aforementioned proposed feature, and in order to use them, you have to enable the TypeScript compiler (tsc) accordingly. As covered in the preface, there are two ways to do this. You can either add the necessary flags on the command line when you invoke tsc or you can configure the necessary options inside the tsconfig.json file.

There are two flags concerning decorators. The first one, experimentalDecorators, is needed to use decorators at all. If you have a file where you're using a decorator and try to compile it without specifying it, you get the following error:

tsc --target es2015 .decorator-example.ts

decorator-example.ts:18:5 – error TS1219:

  Experimental support for decorators is a feature

  that is subject to change in a future release.

  Set the 'experimentalDecorators' option in your 'tsconfig' or

  'jsconfig' to remove this warning.

If you specify the flag, you can compile successfully:

tsc --experimentalDecorators --target es2015

  .decorator-example.ts

In order to avoid specifying the flags all the time, add the following flags in the tsconfig.json file:

{

  "compilerOptions": {

    "target": "ES2015",

    "experimentalDecorators": true,

    "emitDecoratorMetadata": true,

  }

}

Note

Before you begin executing the examples, exercises, and activities, we suggest that you make sure the preceding complier options have been enabled in your tsconfig.json file. Alternatively, you can use the file provided here: https://packt.link/hoeVy.

Importance of Decorators

So, now you're ready to start decorating. But why would you want to do that? Let's run through a simple example that mimics the real-world scenarios you will be encountering later. Let's say that you are building a simple class that will encapsulate the score for a basketball game:

Example_Basketball.ts

1 interface Team {

2 score: number;

3 name: string;

4 }

5

6 class BasketBallGame {

7 private team1: Team;

8 private team2: Team;

9

10 constructor(teamName1: string, teamName2: string) {

11 this.team1 = { score: 0, name: teamName1 };

12 this.team2 = { score: 0, name: teamName2 };

13 }

14

15 getScore() {

16 return `${this.team1.score}:${this.team2.score}`;

17 }

18 }

19

20 const game = new BasketBallGame("LA Lakers", "Boston Celtics");

Our class has two teams, each of which has a name and a numerical score. You're initializing your team in the class constructor, and you have a method that will provide the current score. However, you don't have a method that will update the score. Let's add one:

updateScore(byPoints: number, updateTeam1: boolean) {

    if (updateTeam1) {

        this.team1.score += byPoints;

    } else {

        this.team2.score += byPoints;

    }

}

This method accepts the number of points to add and a Boolean. If the Boolean is true, you're updating the first team's score, and if it's false, you're updating the second team's score. You can take your class for a spin, as here:

const game = new BasketBallGame("LA Lakers", "Boston Celtics");

game.updateScore(3, true);

game.updateScore(2, false);

game.updateScore(2, true);

game.updateScore(2, false);

game.updateScore(2, false);

game.updateScore(2, true);

game.updateScore(2, false);

console.log(game.getScore());

This code will show us that the Lakers are losing 7:8 against the Celtics (Game 7 of the 2010 finals, if anyone wants to know).

The Problem of Cross-Cutting Concerns

So far so good, and your class is fully operational – as far as its own functionalities are concerned. However, as your class will be living within a whole application, you have other concerns as well. One of those concerns is authorization – will just anyone be able to update the score? Of course not, as the common use case is that you have a single person that is allowed to update the score and multiple people, maybe millions, that just watch the score change.

Let's add that concern to the code using a hypothetical function, isAuthorized, that will check whether the current user is actually authorized to change the score. You will call this function and if it returns true, we'll continue with the regular logic of the method. If it returns false, then we'll just issue an appropriate message. The code will look like this:

updateScore(byPoints: number, updateTeam1: boolean) {

    if (isAuthorized()) {

        if (updateTeam1) {

            this.team1.score += byPoints;

        } else {

            this.team2.score += byPoints;

        }

    } else {

        console.log("You're not authorized to change the score");

    }

}

Again, this will work nicely, albeit increasing the code size of your method from five lines of code to nine lines of code and adding some complexity. And, to be honest, the added lines are not really relevant to counting the score, but they had to be added in order to support authorization.

So, is that it? Of course not. Even if you know that somebody is authorized, it does not mean that your operator will be able to update the score whenever they want. The auditor will need detailed information of when and with what parameters the updateScore method was called. No problem, let's add that as well using a hypothetical function called audit. And you'll also need to add some verification for whether the byPoints parameter is a legal value (in basketball, you can only have 1-, 2-, or 3-point increments). And you could add some code that logs the performance of the method in order to have a trace of how long it takes to execute. So, your nice, clear, five-line method will become a 17-line monstrosity:

updateScore(byPoints: number, updateTeam1: boolean) {

    audit("updateScore", byPoints, updateTeam1);

    const start = Date.now();

    if (isAuthorized()) {

        if (validatePoints(byPoints)) {

            if (updateTeam1) {

                this.team1.score += byPoints;

            } else {

                this.team2.score += byPoints;

            }

        } else {

            console.log(`Invalid point value ${byPoints}`);

        }

    } else {

        console.log("You're not authorized to change the score");

    }

    const end = Date.now();

    logDuration("updateScore", start, end);

}

And inside all that complexity, you still have your simple and clear piece of logic that if the Boolean is true, will update the Lakers' score, and if it's false, will update the Celtics' score.

The important part here is that the added complexity does not come from your specific business model – the basketball game still works the same. All the added functionalities stem from the system in which the class lives. The basketball game, by itself, does not need authorization, or performance metrics, or auditing. But the scoreboard application does need all of those and more.

Note that all the added logic is already encapsulated within methods (audit, isAuthorized, logDuration), and the code that actually performs all the aforementioned operations is outside your method. The code you inserted into your function does the bare minimum – yet it still complicated your code.

In addition, authorization, performance metrics, and auditing will be needed in many places within your application, and in none of those places will that code be instrumental to the actual working of the code that is being authorized or measured or audited.

The Solution

Let's take a better look at one of the concerns from the previous section, the performance metric, that is, the duration measurement. This is something that is very important to an application, and to add it to any specific method, you need a few lines of code at the beginning and a few lines at the end of the method:

const start = Date.now();

// actual code of the method

const end = Date.now();

logDuration("updateScore", start, end);

We'll need to add this to each and every method you need to measure. It's very repetitive code, and each time you write it in, you're opening the possibility of doing it slightly wrong. Moreover, if you need to change it, that is, by adding a parameter to the logDuration method, you'll need to change hundreds, if not thousands, of call sites.

In order to avoid that kind of risk, what you can do is to wrap the actual code of the method inside some other function that will still call it. That function might look something like this:

function wrapWithDuration(method: Function) {

    const result = {

        [method.name]: function (this: any, ...args: any[]) {

            const start = Date.now();

            const result = method.apply(this, args);

            const end = Date.now();

            logDuration(method.name, start, end);

            return result;

        },

    };

    return result[method.name];

}

The wrapWithDuration function (whose details you can ignore for now) will take a method and return a function that has the following:

  • The same this reference
  • The same method name
  • The same signature (parameters and return type)
  • All the behavior that the original method has
  • Extended behavior as it will measure the duration of the actual method

Since it will actually call the original method, when looking from outside, the new function is totally indistinguishable from the original. You have added some behavior while keeping everything that already was. Now, you can replace the original method with the new improved one.

What you will get with this approach is this: the original method won't know or care about the cross-cutting concerns of the application, instead focusing on its own business logic – the application can "upgrade" the method at runtime with one that has all the necessary business logic as well as all the required additions.

This kind of transparent "upgrade" is often termed a decoration, and the method that does the decorating is called a decorator method.

What has been shown here is just one form that a decoration can take. There can be as many solutions as there are developers, and none of them will be simple and straightforward. Some standards should be put in place, and the TypeScript design team decided to use the proposed JavaScript syntax.

The rest of the chapter will use that syntax, and you can ignore the solution given here.

Decorators and Decorator Factories

As we've seen so far, decorators are just special wrapping functions that add behavior to your regular methods, classes, and properties. What's special about them is how they can be used in TypeScript. TypeScript supports the following decorator types:

  • Class decorators: These are attached to a class declaration.
  • Method decorators: These are attached to a method declaration.
  • Accessor decorators: These are attached to a declaration of an accessor of a property.
  • Property decorators: These are attached to a property itself.
  • Parameter decorators: These are attached to a single parameter in a method declaration.

And consequently, there are five different places where you can use decorators, so that means that there are five different kinds of special functions that can be used to decorate your code. All of them are shown in the following example:

@ClassDecorator

class SampleClass {

    @PropertyDecorator

    public sampleProperty:number = 0;

    private _sampleField: number = 0;

    @AccessorDecorator

    public get sampleField() { return this._sampleField; }

    @MethodDecorator

    public sampleMethod(@ParameterDecorator paramName: string) {}

}

The sample decorators are functions that are defined as follows:

function ClassDecorator (constructor: Function) {}

function AccessorDecorator (target: any, propertyName: string, descriptor: PropertyDescriptor) {}

function MethodDecorator (target: any, propertyName: string, descriptor: PropertyDescriptor) {}

function PropertyDecorator (target: any, propertyName: string) {}

function ParameterDecorator (target: any, propertyName: string, parameterIndex: number) {}

Decorator Syntax

The syntax for adding a decorator to an item is that you have to use the special symbol @ followed by the name of the decorators. The decorator is placed before the code that it decorates, so in the preceding example, you have performed the following decorations:

  • @ClassDecorator is immediately before the SampleClass class and is a class decorator.
  • @PropertyDecorator is immediately before the public sampleProperty and is a property decorator.
  • @AccessorDecorator is immediately before the public get sampleField() and is a get accessor decorator.
  • @MethodDecorator is immediately before the public sampleMethod() and is a method decorator.
  • @ParameterDecorator is immediately before paramName: string and is a parameter decorator.

While the decorators themselves are regular functions, it's conventional that the names use PascalCase instead of lowerCamelCase.

Note

For more information on PascalCase and lowerCamelCase, visit https://techterms.com/definition/camelcase and https://techterms.com/definition/pascalcase.

Decorator Factories

You can see that you did not specify any parameters for the set of sample decorators in the previous section, yet the decorator function takes between one and three parameters. Those parameters are handled by TypeScript itself and are provided automatically when your code runs. This means that there is no way to configure your decorators directly, for example, by passing additional parameters.

Fortunately, you can use a construct called decorator factories to accomplish that. When decorating, when TypeScript encounters the @ symbol specifying a decorator, it will evaluate the expression that follows. So, instead of providing the name of a function that fits the special decorator requirements, you can provide an expression that will evaluate to such a function. In other words, decorator factories are simply higher-order functions that will return a decorator function.

For example, let's create a simple function that will take a message as a parameter and log a message to the console. The return value of that function, whose input parameters do not conform to the class decorator signature, will be another function, whose input parameters do conform to the class decorator signature. The resulting function will also simply log the message to the console as well. Consider the following code:

Example_Decorator_Factory.ts

1 function ClassDecoratorFactory(message: string) {

2 console.log(`${message} inside factory`);

3 return function (constructor: Function) {

4 console.log(`${message} inside decorator`);

5 };

6 }

In essence, the ClassDecoratorFactory function is not a decorator, but its return value is. This means that you cannot use ClassDecoratorFactory as a decorator itself, but if you call it, for example, ClassDecoratorFactory("Hi"), that value will indeed be a decorator. You can use that to decorate a couple of classes using this syntax. The following example will help you understand this much better:

@ClassDecoratorFactory("Hi")

class DecoratedOne {}

@ClassDecoratorFactory("Hello")

class DecoratedTwo {}

Here, instead of using an expression such as @ClassDecorator as before, you use @ClassDecoratorFactory("hi") or @ClassDecoratorFactory("hello"). Since the result of the execution of the ClassDecoratorFactory function is a class decorator, this is operational, and the decorators successfully decorate the code. You will see the following output when you run your code:

Hi inside factory

Hi inside decorator

Hello inside factory

Hello inside decorator

Note that most decorators that you will use and make will in essence be decorator factories, as it's extremely useful to add parameters when decorating. Most sources and even some documentation will not differentiate between the terms.

Class Decorators

A class decorator is a decorator function that is applied to the whole class. It can be used to observe, change, or replace wholesale a class definition. When a class decorator is called, it receives a single parameter – the constructor function of the calling class.

Property Injection

Property injection is one of the common scenarios that class decorations are used for. For example, let's say you're building a system that will model a school. You will have a class called Teacher that will have the properties and model the behavior of a teacher. The constructor for this class will take two parameters, an id number of the teacher, and the name of the teacher. This is how the class will look:

class Teacher {

    constructor (public id: number, public name: string) {}

    // other teacher specific code

}

Let's say we build the system and it's up and running. Everything is great, but after a while, it's time to update it.

We want to implement an access control system using tokens. Since the new system is not related to the teaching process, it is much better to add it without changing the code of the class itself, so you can use a decorator for this, and your decorator can inject an extra Boolean property to the prototype of the Teacher class. The Teacher class can be changed in the following way:

Example_PropertyInjection.ts

1 @Token

2 class Teacher {

3 // old teacher specific code

4 }

The Token decorator can be defined with the following:

5 function Token (constructor: Function) {

6 constructor.prototype.token = true;

7 }

Now, consider the following code, which creates instances of the class and prints a message:

8 const teacher = new Teacher(1, "John Smith");

9 console.log("Does the teacher have a token? ",teacher["token"]);

Running all this code will give the following result on the console:

Does the teacher have a token? true

In the injection scenario, you use the provided constructor parameter but do not return anything from your function. In this case, the class continues working as it did before. Usually, we'll be using the prototype of the constructor to add fields and properties to the object.

Note

For all exercises and activities in this chapter, before executing the code file, you need to install all dependencies using npm i in the target directory. Then, you can execute the file by running npx ts-node 'filename' in the target directory.

Exercise 7.01: Creating a Simple Class Decorator Factory

In this exercise, you will be creating a simple decorator factory for the Token decorator. Starting from the Teacher class code, we'll create a class called Student that will need to be decorated using the Token decorator. We'll extend the decorator to take a parameter, and decorate both classes using the created decorator factory.

The following steps will help you with the solution:

Note

Before you begin, make sure you have set up the correct compiler options as mentioned in the Setting Up Compiler Options section. The code file for this exercise can also be downloaded from https://packt.link/UpdO9. This repository contains two files: school-token.start.ts and school-token.end.ts. The former contains the code up to step 6 of this exercise, and the latter contains the final code of the exercise.

  1. Open Visual Studio Code, create a new file in a new directory (Exercise01), and save it as school-token.ts.
  2. Enter the following code in school-token.ts:

    @Token

    class Teacher {

        constructor (public id: number, public name: string) {}

        // teacher specific code

    }

    function Token (constructor: Function) {

        constructor.prototype.token = true;

    }

    /////////////////////////

    const teacher = new Teacher(1, "John Smith");

    console.log("Does the teacher have a token? ",teacher["token"]);

  3. Execute the code, and notice that it outputs true to the console.
  4. Add a Student class at the end of the file:

    class Student {

        constructor (public id: number, public name: string) {}

        // student specific code

    }

  5. Add code that creates a student and tries to print its token property:

    const student = new Student(101, "John Bender");

    console.log("Does the student have a token? ",student["token"]);

  6. Execute the code, and notice that it outputs true and undefined to the console.
  7. Add the Token decorator to the Student class:

    @Token

    class Student {//…

  8. Execute the code, and notice that it outputs true twice to the console.
  9. Change the Token function to a factory function that takes a Boolean parameter:

    function Token(hasToken: boolean) {

        return function (constructor: Function) {

            constructor.prototype.token = hasToken;

        }

    }

  10. Modify the Teacher class Token decorator to have a true Boolean parameter:

    @Token(true)

    class Teacher {//…

  11. Modify the Student class Token decorator to have a false Boolean parameter:

    @Token(false)

    class Student {//…

  12. Execute the code by running npx ts-node school-token.ts on the console, and notice that it outputs true and false to the console as shown:

    Does the teacher have a token? true

    Does the student have a token? false

In this exercise, you saw how to add a class decorator that adds a property to a decorated class. You then changed the decorator to use a factory and added two different parameters for two decorated classes. At the end, you verified that the injected properties exist on the decorated classes via the prototype chain and that they have the values you specified.

Constructor Extension

Using property injection enabled you to add behaviors and data to the objects you decorate using their prototypes. That is OK, but sometimes you might want to add data to the constructed objects themselves. You can accomplish this with inheritance, but you can also wrap the inheritance with a decorator.

If you return a function from the decorator, that function will be used as a replacement constructor for the class. While this gives you the superpower to change the class completely, the main goal of this approach is to enable you to augment the class with some new behaviors or data, so let's use automatic inheritance to add properties to the class. A decorator that will add the token property not on the prototype but on the constructed objects themselves would look like this:

type Constructable = {new(...args: any[]):{}};

function Token(hasToken: boolean) {

    return function <T extends Constructable>(constructor: T) {

        return class extends constructor {

            token: boolean = hasToken;

        }

    }

}

The syntax for doing that looks a bit strange at first, as you are using a generic parameter to make sure that the class you return from your decorator will still be compatible with the constructor that was passed as a parameter. Aside from the syntax, the important part to remember is that the code token: boolean = hasToken; will be executed in addition to the regular constructor.

Exercise 7.02: Using a Constructor Extension Decorator

In this exercise, you will be creating a constructor extension decorator factory for the Token decorator. Starting from the Teacher class code, we'll add a token factory called Token that will augment the class by adding a token Boolean property. We'll create an object of the provided class and verify that the object indeed has its own token property. The following steps will help you with the solution:

Note

Before you begin, make sure you have set up the correct compiler options as mentioned in the Setting Up Compiler Options section. The code file for this exercise can also be downloaded from https://packt.link/DhVfC. This repository contains two files: school-token.start.ts and school-token.end.ts. The former contains the code up to step 3 of this exercise, and the latter contains the final code of the exercise.

  1. Open Visual Studio Code, create a new file in a new directory (Exercise02), and save it as school-token.ts.
  2. Enter the following code in school-token.ts:

    class Teacher {

        constructor (public id: number, public name: string) {}

        // teacher specific code

    }

    /////////////////////////

    const teacher = new Teacher(1, "John Smith");

    console.log("Do you have a token:", teacher["token"]);

    console.log("Do you have a token property: ", teacher.hasOwnProperty("token"));

  3. Execute the code, and notice that it outputs undefined and false to the console:

    Do we have a token: undefined

    Do we have a token property: false

  4. Add a Token function at the end of the file:

    type Constructable = {new(...args: any[]):{}};

    function Token(hasToken: boolean) {

        return function <T extends Constructable>(constructor: T) {

            return class extends constructor {

                token: boolean = hasToken;

            }

        }

    }

  5. Decorate the Teacher class using the Token decorator factory:

    @Token(true)

    class Teacher {

  6. Execute the code, and notice that it outputs true twice to the console:

    Do we have a token: true

    Do we have a token property: true

In this exercise, you saw how to change the provided class constructor to run custom code while instantiating an object. You used that to inject a property on the constructed object itself, and then you verified that the injected properties exist on objects of the decorated class and that they have the value you specified.

Constructor Wrapping

Another common scenario for class decorators is the need to just run some code when an instance of a class is being created, for example, to add some logging when an instance of a class is created. You do not need or want to change the class behavior in any way, but you do want to be able to somehow piggyback on the process. This means that you need to execute some code whenever a class constructor is being run – you don't need to change the existing constructor.

In this case, the solution is to have the decorator function return a new constructor that executes the new code needed by the decorator itself as well as the original constructor. For example, if you want to write some text to the console each time you instantiate a decorated class, you can use this decorator:

Example_ConstructorWrapping.ts

1 type Constructable = { new (...args: any[]): {} };

2

3 function WrapConstructor(message: string) {

4 return function <T extends Constructable>(constructor: T) {

5 const wrappedConstructor: any = function (...args: any[]) {

6 console.log(`Decorating ${message}`);

7 const result = new constructor(...args);

8 console.log(`Decorated ${message}`);

9 return result;

10 };

11 wrappedConstructor.prototype = constructor.prototype;

12 return wrappedConstructor;

13 };

14 }

This decorator factory will generate a decorator using a provided message. Since you're returning a new constructor, you have to use a generic parameter to make sure that the constructor you return from your decorator will still be compatible with the constructor that was passed as a parameter. You can create a new wrappedConstructor function within which you can both call custom code (the Decorating and Decorated messages) and actually create the object by calling new on the original constructor, passing in the original arguments.

You should note the following here: it's possible to add custom code both pre- and post-creation of the object. In the preceding example, the Decorating message will be printed to the console before the object is created, while the Decorated message will be printed to the console after the creation is finished.

Another very important thing is that this kind of wrapping breaks the prototype chain of the original object. If the object you decorate thus uses any properties or methods that were available through the prototype chain, they would be missing, changing the behavior of the decorated class. Since that is exactly the opposite of what you wanted to achieve with constructor wrapping, you need to reset the chain. That is done by setting the prototype property of the newly created wrapper function to the prototype of the original constructor.

So, let's use a decorator on a client class, like this:

@WrapConstructor("decorator")

class Teacher {

    constructor(public id: number, public name: string) {

        console.log("Constructing a teacher class instance");

    }

}

Next, you can create an object of the Teacher class:

const teacher = new Teacher(1, "John");

When you run the file, you will see the following written to the console:

Decorating decorator

Constructing a teacher class instance

Decorated decorator

Exercise 7.03: Creating a Logging Decorator for a Class

In this exercise, you'll be creating a constructor wrapping decorator factory for the LogClass decorator. Starting from the Teacher class code, you'll add a decorator factory called LogClass that will wrap the class constructor with some logging code. You'll create an object of the provided class and verify that the logging methods are actually called. The following steps will help you with the solution:

Note

Before you begin, make sure you have set up the correct compiler options as mentioned in the Setting Up Compiler Options section. The code file for this exercise can also be downloaded from https://packt.link/vBLMg.

  1. Open Visual Studio Code, create a new file in a new directory (Exercise03), and save it as teacher-logging.ts.
  2. Enter the following code in teacher-logging.ts:

    class Teacher {

        constructor(public id: number, public name: string) {

            console.log("Constructing a teacher");

        }

    }

    /////////////////////////

    const teacher = new Teacher(1, "John Smith");

  3. Execute the code, and notice that it outputs Constructing a teacher to the console.
  4. Next, create the decorator. First, you need to add the Constructable type definition:

    type Constructable = { new (...args: any[]): {} };

  5. Now, add a definition of your decorator factory:

    function LogClass(message: string) {

        return function <T extends Constructable>(constructor: T) {

            return constructor;

        };

    }

    In the preceding code, the constructor takes in a string parameter and returns a decorator function. The decorator function itself will initially just return the original, unchanged constructor of the decorated class.

  6. Decorate the Teacher class using the LogClass decorator with an appropriate message parameter:

    @LogClass("Teacher decorator")

    class Teacher {

        constructor(public id: number, public name: string) {

            console.log("Constructing a teacher");

        }

    }

  7. Execute the code, and notice that there are no changes to the behavior.
  8. Now, add a logger object to your application:

    const logger = {

        info: (message: string) => {

            console.log(`[INFO]: ${message}`);

        },

    };

    In actual production-grade code implementation, you might log to a database, a file, a third-party service, and so on. In the preceding step, you are simply logging to the console.

  9. Next, use the logger object to add a wrapping constructor to your decorator:

        return function <T extends Constructable>(constructor: T) {

            const loggingConstructor: any = function(...args: any[]){

                logger.info(message);

                return new constructor(...args);

            }

            loggingConstructor.prototype = constructor.prototype;

            return loggingConstructor;

        };

  10. Execute the code and verify that you get a logging message to the console:

    [INFO]: Teacher decorator

    Constructing a teacher

  11. Construct a few more objects and verify that the constructor runs each time an object is created:

    for (let index = 0; index < 10; index++) {

        const teacher = new Teacher(index +1, "LouAnne Johnson");

    }

    You'll see the following output when you execute the file:

    [INFO]: Teacher decorator

    Constructing a teacher

    [INFO]: Teacher decorator

    Constructing a teacher

    [INFO]: Teacher decorator

    Constructing a teacher

    [INFO]: Teacher decorator

    Constructing a teacher

    [INFO]: Teacher decorator

    Constructing a teacher

    [INFO]: Teacher decorator

    Constructing a teacher

    [INFO]: Teacher decorator

    Constructing a teacher

In this exercise, you saw how to wrap the provided class constructor so that it can run custom code, but without changing the construction of the objects. Through wrapping, you added logging capabilities to a class that did not have any. You constructed objects of that class and verified that the logging functionality was operational.

Method and Accessor Decorators

A method decorator is a decorator function that is applied to a single method of a class. In a method decorator, you can observe, modify, or outright replace a method definition with one provided by the decorator. When a method decorator is called, it receives three parameters: target, propertyKey, and descriptor:

  • target: Since methods can be both instance methods (defined on instances of the class) and static methods (defined on the class itself), target can be two different things. For instance methods, it's the prototype of the class. For static methods, it's the constructor function of the class. Usually, you type this parameter as any.
  • propertyKey: This is the name of the method you're decorating.
  • descriptor: This is the property descriptor of the method you're decorating. The PropertyDescriptor interface is defined with this:

    interface PropertyDescriptor {

        configurable?: boolean;

        enumerable?: boolean;

        value?: any;

        writable?: boolean;

        get?(): any;

        set?(v: any): void;

    }

This interface defines the value of an object property, as well as the property's properties (whether the property is configurable, enumerable, and writable). We'll also be using a typed version of this interface, TypedPropertyDescriptor, which is defined as shown:

interface TypedPropertyDescriptor<T> {

    enumerable?: boolean;

    configurable?: boolean;

    writable?: boolean;

    value?: T;

    get?: () => T;

    set?: (value: T) => void;

}

Note that, in JavaScript, and subsequently TypeScript, property accessors are just special methods that manage access to a property. Everything that is applicable to decorating methods is also applicable to decorating accessors. Any accessor specifics will be covered separately.

If you set up a decorator on a method, we'll be getting the PropertyDescriptor instance of the method itself, and the value property of the descriptor will give us access to its body. If you set up a decorator on an accessor, we'll be getting the PropertyDescriptor instance of the corresponding property, with its get and set properties respectively set to the getter and setter accessors. This means that if you're decorating property accessors, you don't have to separately decorate the getter and the setter, as any decoration of one is a decoration on the other. In fact, TypeScript will issue the following error if you do so:

TS1207: Decorators cannot be applied to multiple get/set accessors of the same name.

The method decorators do not have to return a value, as most of the time you can do the desired actions by modifying the property descriptor. If you do return a value, however, that value will replace the originally provided property descriptor.

Decorators on Instance Functions

As described in the preceding section, any function that takes the target, propertyKey, and descriptor parameters can be used to decorate methods and property accessors. So, let's have a function that will simply log the target, propertyKey, and descriptor parameters to the console:

Example_Decorators_Instance_Functions.ts

1 function DecorateMethod(target: any, propertyName: string,

2 descriptor: PropertyDescriptor) {

3 console.log("Target is:", target);

4 console.log("Property name is:", propertyName);

5 console.log("Descriptor is:", descriptor);

6 }

You can use this function to decorate a class' methods. This is an extremely simple decorator, but you can use it to investigate the usage of method decorators.

Let's start with a simple class:

class Teacher {

    constructor (public name: string){}

    private _title: string = "";

    public get title() {

        return this._title;

    }

    

    public set title(value: string) {

        this._title = value;

    }

    public teach() {

        console.log(`${this.name} is teaching`)

    }

}

The class has a constructor, a method called teach, and a title property with a defined getter and setter. The accessors simply pass through control to the _title private field. You can add the decorator to the teach methods using the following code:

    @DecorateMethod

    public teach() {

        // ....

When you run your code (no need to instantiate the class), you'll get the following output on the console:

    Target is: {}

    Property name is: teach

    Descriptor is: {

        value: [Function: teach],

        writable: true,

        enumerable: false,

        configurable: true

    }

Consider the following snippets in which you apply the decorator to the setter or getter (either one will work fine, but not both):

    @DecorateMethod

    public get title() {

        // ....

Or:

    @DecorateMethod

    public set title(value: string) {

        // ....

You will get the following output when you run the code using either of the preceding suggestions:

    Target is: {}

    Property name is: title

    Descriptor is: {

        get: [Function: get title],

        set: [Function: set title],

        enumerable: false,

        configurable: true

    }

Note that you cannot add a method decorator on the constructor itself, as you will get an error:

TS1206: Decorators are not valid here.

If you need to change the behavior of the constructor, you should use class decorators.

Exercise 7.04: Creating a Decorator That Marks a Function Enumerable

In this exercise, you will create a decorator that will be able to change the enumerable state of the methods and accessors that it decorates. You will use this decorator to set the enumerable state of some functions in a class that you'll write, and finally, you'll verify that when you enumerate the properties of the object instance, you get the modified methods as well.

Note

Before you begin, make sure you have set up the correct compiler options as mentioned in the Setting Up Compiler Options section. The code file for this exercise can also be downloaded from https://packt.link/1nAff. This repository contains two files: teacher-enumerating.start.ts and teacher-enumerating.end.ts. The former contains the code up to step 5 of this exercise, and the latter contains the final code of the exercise.

  1. Open Visual Studio Code, create a new file in a new directory (Exercise04), and save it as teacher-enumerating.ts.
  2. Enter the following code in teacher-enumerating.ts:

    class Teacher {

        constructor (public name: string){}

        private _title: string = "";

        public get title() {

            return this._title;

        }

        

        public set title(value: string) {

            this._title = value;

        }

        public teach() {

            console.log(`${this.name} is teaching`)

        }

    }

  3. Write code that will instantiate an object of this class:

    const teacher = new Teacher("John Smith");

  4. Write code that will enumerate all the keys in the created object:

    for (const key in teacher) {

        console.log(key);

    }

  5. Execute the file and verify that the only keys that are displayed on the console are name and _title.
  6. Add a decorator factory that takes a Boolean parameter and generates a method decorator that will set the enumerable status to the provided parameter:

    function Enumerable(value: boolean) {

        return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {

            descriptor.enumerable = value;

        }

    };

  7. Use the decorator to decorate the title getter or setter accessors and the teach method:

        @Enumerable(true)

        public get title() {

            return this._title;

        }

        

        public set title(value: string) {

            this._title = value;

        }

        @Enumerable(true)

        public teach() {

            console.log(`${this.name} is teaching`)

        }

  8. Rerun the code and verify that the title and teach properties are being enumerated:

    name

    _title

    title

    teach

In this exercise, you saw how to add a create a method decorator factory and how to apply it to an instance method or an instance property accessor. You learned how to make a property enumerable, and you used that knowledge to set the enumerable state of the functions of a class. Finally, you enumerated all the properties of a class.

Decorators on Static Functions

Just like with instance methods, decorators can be used with static methods as well. You add a static method to your Teacher class like this:

Example_Decorator_StaticFunctions.ts

1 class Teacher {

2 //.....

3

4 public static showUsage() {

5 console.log("This is the Teacher class")

6 }

7 //.....

We are allowed to use method decorators on the static methods as well. So, you can add the DecorateMethod decorator using the following code:

    @DecorateMethod

    public static showUsage() {

        //......

When you run the code, you will get output similar to this:

Target is: [Function: Teacher]

Property name is: showUsage

Descriptor is: {

  value: [Function: showUsage],

  writable: true,

  enumerable: false,

  configurable: true

}

The principal difference with the instance methods is the target parameter. Instance methods and accessors are generated on the class prototype, and consequently, when using a method/accessor decorator, you receive the class prototype as a target parameter. Static methods and accessors are generated on the class variable itself, and consequently, when using a method/accessor decorator, you receive the class variable in the guise of the constructor function as a target parameter.

Note that this is the exact same object that you're getting as a class decorator parameter. You can even use it in much the same way. However, in method decorators, the focus should be on the actual property we've decorated. It is considered a bad practice to manipulate the constructor inside a non-class decorator.

Method Wrapping Decorators

The most common usage of method decorators is to use it to wrap the original method, adding some custom cross-cutting code. Examples would be adding some general error handling or adding automatic logging capabilities.

In order to do that, you need to change the function that is being called. You can do that using the value property of method property descriptors, and by using the get and set properties of the property accessor descriptors.

Exercise 7.05: Creating a Logging Decorator for a Method

In this exercise, you'll be creating a decorator that will log each time a decorated method or accessor is called. You will use this decorator to add logging to the Teacher class and you'll verify that each time you use the decorated methods and property accessors, you get an appropriate log entry:

Note

Before you begin, make sure you have set up the correct compiler options as mentioned in the Setting Up Compiler Options section. The code file for this exercise can also be downloaded from https://packt.link/rmEZi.

  1. Open Visual Studio Code, create a new file in a new directory (Exercise05), and save it as teacher-logging.ts.
  2. Enter the following code in teacher-logging.ts:

    class Teacher {

        constructor (public name: string){}

        private _title: string = "";

        public get title() {

            return this._title;

        }

        

        public set title(value: string) {

            this._title = value;

        }

        public teach() {

            console.log(`${this.name} is teaching`)

        }

    }

    /////////////////

    const teacher = new Teacher("John Smith");

    teacher.teach(); // we're invoking the teach method

    teacher.title = "Mr." // we're invoking the title setter

    console.log(`${teacher.title} ${teacher.name}`); // we're invoking the title getter

  3. Execute the code, and notice that it outputs John Smith is teaching and Mr. John Smith to the console.
  4. Create a method decorator factory that can wrap any method, getter or setter, with a logging statement. It will take a string parameter and return a decorator function. Initially, you won't make any changes to the property descriptor:

    function LogMethod(message: string) {

        return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {

        };

    }

  5. Decorate the teach method and the title get accessor using the LogMethod decorator with an appropriate message parameter:

        @LogMethod("Title property")

        public get title() {

        //...

        @LogMethod("Teach method")

        public teach() {

        //...

  6. Execute the code, and notice that there are no changes to the behavior.
  7. Now, add a logger object to your application:

    const logger = {

        info: (message: string) => {

            console.log(`[INFO]: ${message}`);

        },

    };

    In an actual production-grade implementation, you might log to a database, a file, a third-party service, and so on. In the preceding step, you are simply logging to the console.

  8. Add code to the decorator factory that will wrap the property descriptors, value, get, and set properties (if they are present):

    function LogMethod(message: string) {

        return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {

            if (descriptor.value) {

                const original = descriptor.value;

                descriptor.value = function (...args: any[]) {

                    logger.info(`${message}: Method ${propertyName} invoked`);

                    // we're passing in the original arguments to the method

                    return original.apply(this, args);

                }

            }

            if (descriptor.get) {

                const original = descriptor.get;

                descriptor.get = function () {

                    logger.info(`${message}: Getter for ${propertyName} invoked`);

                    // getter accessors do not take parameters

                    return original.apply(this, []);

                }

            }

            if (descriptor.set) {

                const original = descriptor.set;

                descriptor.set = function (value: any) {

                    logger.info(`${message}: Setter for ${propertyName} invoked`);

                    // setter accessors take a single parameter, i.e. the value to be set

                    return original.apply(this, [value]);

                }

            }

        }

    }

  9. Execute the code and verify that you get logging messages to the console when you call the method as well as when you use the title property:

    [INFO]: Teach method: Method teach invoked

    John Smith is teaching

    [INFO]: Title property: Setter for title invoked

    [INFO]: Title property: Getter for title invoked

    Mr. John Smith

In this exercise, you saw how to wrap the provided definitions of methods and property accessors class in such a way that you could run custom code on every invocation without changing the behavior of the functions themselves. You used that to add logging capabilities to functions that did not have any. You constructed objects of that class and verified that the logging functionality is operational.

Activity 7.01: Creating Decorators for Call Counting

As a developer of a backend service for a website, you are tasked with creating a solution that will enable the operations department to have clear auditing on the behavior of the service. For that, the app is required to have a tally of all class instantiations and method invocations.

In this activity, you're going to create class and method decorators that can be used to count class instantiations and method invocations. You will create a class that contains data about a person and use the decorators to count how many such objects were created and how many times each method was called. After you have constructed several objects and used their properties, take a look at the values of the counters.

The aim of this activity is to demonstrate the uses of class and method decorators in order to address a cross-cutting concern of your application, without changing the functionality of the given class. You should have a detailed statistic of the life cycles of your objects, without adding any complexity to the business logic.

The following steps should help you with the solution:

Note

Before you begin, make sure you have set up the correct compiler options as mentioned in the Setting Up Compiler Options section. The code file for this activity can also be downloaded from https://packt.link/UK49t.

  1. Create a class called Person with public properties named firstName, lastName, and birthday.
  2. Add a constructor that initializes the properties via the constructor parameters.
  3. Add a private field called _title and expose it via a getter and setter as a property called title.
  4. Add a method called getFullName that will return the full name of a person.
  5. Add a method called getAge that will return the current age of the person (by subtracting the birthday from the current year).
  6. Create a global object called count and initialize it to the empty object. This will be your state variable, where you store the counts for every instantiation and invocation.
  7. Create a constructor wrapping decorator factory called CountClass that will take a string parameter called counterName. We'll use that parameter as a key into the count object.
  8. Inside the wrapping code, increase the count object's property defined in the counterName parameter by 1.
  9. Don't forget to set the prototype chain of the wrapped constructor.
  10. Create a method wrapping decorator factory called CountMethod that will take a string parameter called counterName.
  11. Add checks for whether the descriptor parameter has value, get, and set properties. You need to cover both the cases where this decorator is used as an accessor and as a method decorator.
  12. In each respective branch, add code that wraps the method.
  13. Inside the wrapping code, increase the count object's property defined in the counterName parameter by 1.
  14. Decorate the class using the CountClass decorator, with a person parameter.
  15. Decorate getFullName, getAge, and the title property getter with the CountMethod decorator, using the person-full-name, person-age, and person-title parameters, respectively. Note that you need to decorate only one of the property accessors.
  16. Write code outside the class that will instantiate three person objects.
  17. Write code that will call the getFullName and getAge methods on the objects
  18. Write code that will check whether the title property is empty and set it to something if it is.
  19. Write code that will log the count object to the console in order to see if your decorators are running correctly.

The expected output is as follows:

{

    person: 3,

    "person-full-name": 3,

    "person-age": 3,

    "person-title": 6

}

This activity demonstrates the power of using decorators to extend and augment the capabilities of your classes without polluting the code. You were able to inject custom code execution into your objects, without changing any of the underlying business logic.

Note

The solution to the activity can be found via this link.

Using Metadata in Decorators

So far, you've been decorating classes and methods. These are basically pieces of code that get executed, and you have been able to change and augment the code that got executed. But your code consists not only of "active," live code, but of other definitions as well – in particular, your classes have fields, and your methods have parameters. In the activity before this section, you were able to detect whenever the title property was accessed because you had a method that was getting the value, and a method that was setting the value – so you piggybacked your code to the already existing "active" code. But how do you decorate the "passive" parts of your program? You cannot attach code that runs when your "passive" code gets executed, because frankly there's nothing to execute in public firstName: string. It's a simple definition.

You cannot attach any code that gets executed for your "passive code," but what you can do using decorators is add some data to some global object regarding the decorated "passive" piece of code. In Activity 7.01: Creating Decorators for Call Counting, you defined a global count object and used that in your decorators to keep track of the executions. That approach works, but it requires creating a global variable, which is bad in most cases. It would be much cleaner if you were able to define some kind of properties on the methods and classes themselves. But, on the other hand, you don't want to add too many properties that are available alongside the business logic code – the possibility of incidental error is too high. What you need is to be able to somehow add metadata to your classes and methods.

Fortunately, this is a common problem and there is a proposal to add proper metadata support to JavaScript. In the meantime, there is a polyfill library called reflect-metadata that can be used.

Note

For more information on the reflect-metadata library, visit https://www.npmjs.com/package/reflect-metadata.

What this library does, in essence, is attach a special property to your classes that gives us a place to store, retrieve, and work with metadata about your class.

In TypeScript, in order to use this feature, you have to specify an additional compiler flag, either via the command line or via tsconfig.json. That is the emitDecoratorMetadata flag, which needs to be set to true in order to work with the metadata methods.

Reflect Object

The API of the reflect-metadata library is straightforward, and mostly you can focus on the following methods:

  • Reflect.defineMetadata: Defines a piece of metadata on a class or a method
  • Reflect.hasMetadata: Returns a Boolean indicating whether a certain piece of metadata is present
  • Reflect.getMetadata: Returns the actual piece of metadata, if present

Consider the following code:

class Teacher {

    constructor (public name: string){}

    private _title: string = "";

    public get title() {

        return this._title;

    }

    

    public set title(value: string) {

        this._title = value;

    }

    public teach() {

        console.log(`${this.name} is teaching`)

    }

}

Here you have a class called Teacher that has a simple private field, _title, which has get and set accessor methods for a property called title, and a method called teach that logs to the console that the teacher is, in fact, teaching.

You can define a metadata key called call-count on the Teacher class and set its value to 0 by executing the following call to defineMetadata:

Reflect.defineMetadata("call-count", 0, Teacher);

If you want to add a metadata key called call-count, not on the Teacher class itself but on the teach method, you could do so with the following call to defineMetadata:

Reflect.defineMetadata("call-count", 10, Teacher, "teach");

This will define a metadata key called call-count on the Teacher class' teach property and set its value to 10. You can retrieve these values using the following commands:

Reflect.getMetadata("call-count", Teacher); // will return 0

Reflect.getMetadata("call-count", Teacher, "teach"); // will return 10

In essence, you can create a method that will register a call of a method with the following code:

function increaseCallCount(target: any, propertyKey: string) {

    if (Reflect.hasMetadata("call-count", target)) {

        const value = Reflect.getMetadata("call-count", target, propertyKey);

        Reflect.defineMetadata("call-count", value+1, target, propertyKey)

    } else {

        Reflect.defineMetadata("call-count", 1, target, propertyKey)

    }

}

This code will first call the hasMetadata method, to check whether you have already defined a value for the call-count metadata. If that is true, the hasMetadata method will call getMetadata to get the current value and then call defineMetadata to re-define the metadata property with an increased (value+1) value. If you did not have such a metadata property, the defineMetadata method will define it with a value of 1.

When called with increaseCallCount(Teacher, "teach");, it will successfully increase the call count of the teach method of the Teacher class. The metadata added to the class will in no way hinder the behaviors that the class already has, so any code that is being executed won't be affected.

Exercise 7.06: Adding Metadata to Methods via Decorators

In this exercise, we'll create a simple class and apply some metadata for describing its methods. After you have done this, you will write a function that given a class, will display its available descriptions:

Note

Before you begin, make sure you have set up the correct compiler options as mentioned in the Setting Up Compiler Options section. The code file for this exercise can also be downloaded from https://packt.link/JG4F8.

  1. Open Visual Studio Code, create a new file in a new directory (Exercise06), and save it as calculator-metadata.ts.
  2. Enter the following code in calculator-metadata.ts:

    class Calculator {

        constructor (public first: number, public second: number) {}

        public add() {

            return this.first + this.second;

        }

        public subtract() {

            return this.first – this.second;

        }

        public multiply() {

            return this.first / this.second;

        }

        public divide() {

            return this.first / this.second;

        }

    }

  3. Next, add metadata descriptions for the class and some of its methods:

    Reflect.defineMetadata("description", "A class that offers common operations over two numbers", Calculator);

    Reflect.defineMetadata("description", "Returns the result of adding two numbers", Calculator, "add");

    Reflect.defineMetadata("description", "Returns the result of subtracting two numbers", Calculator, "subtract");

    Reflect.defineMetadata("description", "Returns the result of dividing two numbers", Calculator, "divide");

  4. Define a function that when given a class will reflect upon it and extract and display the class' description metadata:

    function showDescriptions (target: any) {

        if (Reflect.hasMetadata("description", target)) {

            const classDescription = Reflect.getMetadata("description", target);

            console.log(`${target.name}: ${classDescription}`);

        }

    }

  5. Call the function using showDescriptions(Calculator); and verify that it will display the following output:

    Calculator: A class that offers common operations over two numbers

    In order to get a list of all methods of a class, we'll have to use the Object.getOwnPropertyNames function. Additionally, since the methods are actually defined on the prototype of the class, the correct line that gets all methods names of a class is const methodNames = Object.getOwnPropertyNames(target.prototype);.

  6. Next, loop over the returned array and check each method for a description. The showDescription function will now have the following format:

    function showDescriptions (target: any) {

        if (Reflect.hasMetadata("description", target)) {

            const classDescription = Reflect.getMetadata("description", target);

            console.log(`${target.name}: ${classDescription}`);

            const methodNames = Object.getOwnPropertyNames(target.prototype);

            for (const methodName of methodNames) {

                if (Reflect.hasMetadata("description", target, methodName)) {

                    const description = Reflect.getMetadata("description", target, methodName);

                    console.log(` ${methodName}: ${description}`);

                }

            }

        }

    }

  7. Call the function again and verify that it will display the following output:

    Calculator: A class that offers common operations over two numbers

      add: Returns the result of adding two numbers

      subtract: Returns the result of subtracting two numbers

      divide: Returns the result of dividing two numbers

Note that you're not displaying anything for the multiply method, as you did not add any metadata for it.

In this exercise, you learned how to add metadata to classes and methods and how to check its existence and, if present, to retrieve it. You also managed to get a list of all the methods of a given class.

Property Decorators

A property decorator is a decorator function that is applied to a single property of a class. Unlike in a method or class decorators, you cannot modify or replace the property definition, but you can indeed observe it.

Note

Since you receive the constructor function in the decorator, this is not strictly true. You could change the code of the class, but it's extremely inadvisable.

When a property decorator is called, it receives two parameters: target and propertyKey:

  • target: Since properties can be both instance properties (defined on instances of the class) and static properties (defined on the class itself), target can be two different things. For instance properties, it's the prototype of the class. For static properties, it's the constructor function of the class. Usually, you would type this parameter as any.
  • propertyKey: This is the name of the property you're decorating.

In contrast to the method decorators, you're not receiving a property descriptor parameter, because, plainly, there isn't one available. Also, because you do not return any code that can be replaced, the return value of a property decorator is ignored.

For example, you can define a simple property decorator factory that just logs a message to the console to notify that the property is actually decorated:

Example_PropertyDecorators.ts

1 function DecorateProperty(message: string) {

2 return function (target: any, propertyKey: string) {

3 console.log(`Decorated

4 ${target.constructor.name}.${propertyKey} with '${message}'`);

5 }

6 }

Consider the following class definitions:

class Teacher {

    public id: number;

    public name: string;

    constructor(id: number, name: string) {

        this.id = id;

        this.name = name;

    }

}

You can annotate the id and name properties using the following code:

    @DecorateProperty("ID")

    public id: number;

    @DecorateProperty("NAME")

    public name: string;

If you now execute the code (we don't need to call anything; it will be called by the TypeScript engine), you obtain the following output:

Decorated Teacher.id with 'ID'

Decorated Teacher.name with 'NAME'

Note that you did not create any objects of the teacher class, or call any methods. The decorators executed when the class was defined. Since property decorators are passive, usually you'll use them to feed some kind of data into some mechanism that will use it. One of the common approaches is to combine the passive decorators with one or several active decorators, that is, class and method decorators.

Note

This is the case in Angular, for example, where the passive @Input and @Output decorators are combined with the active @Component decorator.

Another common use case is to have an additional mechanism that will get the data provided by the decorators and use it. For example, you can have the decorators recording some metadata, and then have another function that reads and uses that metadata.

Exercise 7.07: Creating and Using a Property Decorator

In this exercise, you'll create a simple property decorator factory that will provide each property with a description. After you have done this, you will write a function that given a class will display its available descriptions:

Note

Before you begin, make sure you have set up the correct compiler options as mentioned in the Setting Up Compiler Options section. The code file for this exercise can also be downloaded from https://packt.link/1WU6d.

  1. Open Visual Studio Code, create a new file in a new directory (Exercise07), and save it as teacher-properties.ts.
  2. Enter the following code in teacher-properties.ts:

    class Teacher {

        public id: number;

        public name: string;

        constructor(id: number, name: string) {

            this.id = id;

            this.name = name;

        }

    }

  3. Add a decorator factory that takes a string parameter and generates a property decorator that will add a metadata description field to the class for the given property:

    function Description(message: string) {

        return function (target: any, propertyKey: string) {

            Reflect.defineMetadata("description", message, target, propertyKey)

        }

    }

  4. Next, annotate the properties of the Teacher class using the description:

        @Description("This is the id of the teacher")

        public id: number;

        @Description("This is the name of the teacher")

        public name: string;

  5. Define a function that, when given an object, will reflect upon it and extract and display the description metadata for the object's properties:

    function showDescriptions (target: any) {

        for (const key in target) {

            if (Reflect.hasMetadata("description", target, key)) {

                const description = Reflect.getMetadata("description", target, key);

                console.log(` ${key}: ${description}`);

            }

        }

    }

  6. Create an object of the Teacher class:

    const teacher = new Teacher(1, "John Smith");

  7. Pass that object to the showDescriptions function:

    showDescriptions(teacher);

  8. Execute the code and verify that the descriptions are displayed:

      id: This is the id of the teacher

      name: This is the name of the teacher

In this exercise, you learned how to add metadata to properties using property decorators and how to use property decorators to add quick basic documentation to your classes.

Parameter Decorators

A parameter decorator is a decorator function that is applied to a single parameter of a function call. Just like property decorators, parameter decorators are passive, that is, they can be used only to observe values, but not to inject and execute code. The return value of a parameter decorator is similarly ignored. As a consequence, parameter decorators are almost exclusively used in conjunction with other, active decorators.

When a parameter decorator is called, it receives three parameters: target, propertyKey, and parameterIndex:

  • target: The behavior for this parameter is identical to the decorators on the corresponding method. There is an exception if the parameter is on a class' constructor, but that is explained shortly.
  • propertyKey: This is the name of the method whose parameter you're decorating (the constructor exception is explained shortly).
  • parameterIndex: This is the ordinal index of the parameter in the function's parameter list (starting with zero for the first parameter).

So, let's have a function that will simply log the target, propertyKey, and parameterIndex parameters to the console:

Example_ParameterDecorators.ts

1 function DecorateParam(target: any, propertyName: string,

2 parameterIndex: number) {

3 console.log("Target is:", target);

4 console.log("Property name is:", propertyName);

5 console.log("Index is:", parameterIndex);

6 }

You can use this function to decorate a function's parameters and can investigate the usage of parameter decorators. Let's start with a simple class:

class Teacher {

    public id: number;

    public name: string;

    constructor(id: number, name: string) {

        this.id = id;

        this.name = name;

    }

    public getFullName(title: string, suffix: string) {

        return `${title} ${this.name}, ${suffix}`

    }

}

The class has a constructor that takes two parameters, id and name, and a method called getFullName, which takes two parameters, title and suffix. Say you add your decorator to the first parameter of the getFullName methods, using this:

     public getFullName(@DecorateParam title: string, suffix: string) {

        // ....

If you run your code (no need to instantiate the class), you'll get the following output on the console:

Target is: Teacher {}

Property name is: getFullName

Index is: 0

We can also apply parameter decorators to the parameters of the constructor function itself. Say you decorate the second constructor parameter, like this:

    constructor(id: number, @DecorateParam name: string) {

        // ....

You will get the following output when you run the code:

Target is: [Function: Teacher]

Property name is: undefined

Index is: 1

Note that in this case, the target is not the prototype of the class, but the class constructor itself. Also, when decorating constructor parameters, the name of the property is undefined.

Exercise 7.08: Creating and Using a Parameter Decorator

In this exercise, you will create a parameter decorator that will indicate that a certain parameter is required; that is, it should not have an empty value. You will also create a validation decorator for the method, so that the validation can actually take place. We'll create a class that uses the decorators, and you will try to call the method with both valid and invalid values:

Note

Before you begin, make sure you have set up the correct compiler options as mentioned in the Setting Up Compiler Options section.The code file for this exercise can also be downloaded from https://packt.link/Hf3fv.

  1. Open Visual Studio Code, create a new file in a new directory (Exercise08), and save it as teacher-parameters.ts.
  2. Enter the following code in teacher-parameters.ts:

    class Teacher {

        public id: number;

        public name: string;

        constructor(id: number, name: string) {

            this.id = id;

            this.name = name;

        }

        public getFullName(title: string, suffix: string) {

            return `${title} ${this.name}, ${suffix}`

        }

    }

  3. Create a parameter decorator called Required that will add the index of the parameter to the required metadata field to the class for the given property:

    function Required(target: any, propertyKey: string, parameterIndex: number) {

        if (Reflect.hasMetadata("required", target, propertyKey)) {

            const existing = Reflect.getMetadata("required", target, propertyKey) as number[];

            Reflect.defineMetadata("required", existing.concat(parameterIndex), target, propertyKey);

        } else {

            Reflect.defineMetadata("required", [parameterIndex], target, propertyKey)

        }

    }

    Here, if the metadata already exists, that means that there is another required parameter. If so, you load it and concatenate your parameterIndex. If there is no previous metadata, you define it with an array consisting of your parameterIndex.

  4. Next, create a method decorator that will wrap the original method and check all required parameters before calling the original method:

    function Validate(target: any, propertyKey:string, descriptor: PropertyDescriptor) {

        const original = descriptor.value;

        descriptor.value = function (...args: any[]) {

            // validate parameters

            if (Reflect.hasMetadata("required", target, propertyKey)) {

                const requiredParams = Reflect.getMetadata("required", target, propertyKey) as number[];

                for (const required of requiredParams) {

                    if (!args[required]) {

                        throw Error(`The parameter at position ${required} is required`)

                    }

                }

            }

            return original.apply(this, args);

        }

    }

    If any of your required parameters has a falsy value, instead of executing the original method, your decorator will throw an error.

  5. After that, annotate the title parameter of the getFullName method with the Required decorator and the method itself with the Validate decorator:

        @Validate

        public getFullName(@Required title: string, suffix: string) {

            // ....

  6. Create an object of the Teacher class:

    const teacher = new Teacher(1, "John Smith");

  7. Try to call the getFullName method with an empty string as the first parameter:

    try {

         console.log(teacher.getFullName("", "Esq"));

    } catch (e) {

         console.log(e.message);

    }

  8. Execute the code and verify that the error message is displayed instead:

    The parameter at position 0 is required

In this exercise, you covered how to create parameter decorators and how to use them to add metadata. You also orchestrated the usage of the same metadata into another decorator, and build a basic validation system.

Application of Multiple Decorators on a Single Target

It is often necessary to apply more than one decorator on a single target. And as decorators can (and do) change the code that actually gets executed, it's important to have an understanding of how different decorators play together.

Basically, decorators are functions, and you're using them to compose your targets. This means that, in essence, decorators will be applied and executed bottom-up, with the decorator that's closest to the target going first and providing the result for the second decorators, and so on. This is similar to functional composition; that is, when we're trying to calculate f(g(x)), first the g function will be called, and then the f function will be called.

There is a small catch when using decorator factories, though. The composition rule only applies to the decorators themselves – and decorator factories are not decorators per se. They are functions that need to be executed in order to return a decorator. This means that they are executed in source code order, that is, top-down. Imagine that you have two decorator factories:

Example_MultipleDecorators.ts

1 function First () {

2 console.log("Generating first decorator")

3 return function (constructor: Function) {

4 console.log("Applying first decorator")

5 }

6 }

Second decorator factory:

7 function Second () {

8 console.log("Generating second decorator")

9 return function (constructor: Function) {

10 console.log("Applying second decorator")

11 }

12 }

Now imagine that they are applied on a single target:

13 @First()

14 @Second()

15 class Target {}

The generation process will generate the first decorator before the second, but in the application process, the second will be applied, and then the first:

Generating first decorator

Generating second decorator

Applying second decorator

Applying first decorator

Activity 7.02: Using Decorators to Apply Cross-Cutting Concerns

In this activity, we're going full circle to the basketball game example (Example_Basketball.ts). You are tasked with adding all the necessary cross-cutting concerns, such as authentication, performance metrics, auditing, and validation to the Example_Basketball.ts file in a maintainable manner.

You can begin the activity with the code that you already have in the Example_Basketball.ts. First, take stock of the elements that are already present in the file:

  • The interface that describes the team.
  • The class for the game itself. You have a constructor that creates the team objects given the team names. You also have a getScore function that displayed the score and a simple updateScore method that updates the score of the game, taking the scoring team and the score value as parameters.

Now you need to add the cross-cutting concerns as mentioned previously without changing the code of the class itself, only by using decorators.

Earlier in Example_Basketball.ts, you had to completely subsume the business logic of keeping score under the code that was needed to address everything else (such as authorization, auditing, metrics, and so on). Now apply all the decorator skills that are needed so that the application runs properly but still has a crisp and clear codebase.

Note

The code file for this activity can also be downloaded from https://packt.link/7KfCx.

The following steps should help you with the solution:

  1. Create the code for the BasketBallGame class.
  2. Create a class decorator factory called Authenticate that will take a permission parameter and return a class decorator with constructor wrapping. The class decorator should load the permissions metadata property (array of strings), then check if the passed parameter is an element of the array. If the passed parameter is not an element of the array, the class decorator should throw an error, and if it's present, it should continue with the class creation.
  3. Define a metadata property of the BasketballGame class called permissions with the value ["canUpdateScore"].
  4. Apply the class decorator factory on the BasketballGame class with a parameter value of canUpdateScore.
  5. Create a method decorator called MeasureDuration that will use method wrapping to start a timer before the method body is executed and stop it after it's done. It should calculate the duration and push it to a metadata property called durations for the method.
  6. Apply the MeasureDuration method decorator on the updateScore method.
  7. Create a method decorator factory called Audit that will take a message parameter and return a method decorator. The method decorator should use method wrapping to get the arguments and the return value of the method. After the successful execution of the original method, it should display the audit log to the console.
  8. Apply the Audit method decorator factory on the updateScore method, with a parameter value of Updated score.
  9. Create a parameter decorator called OneTwoThree that will add the decorated parameter in the one-two-three metadata property.
  10. Create a method decorator called Validate that will use method wrapping to load all values for the one-two-three metadata property, and for all marked parameters check their value. If the value is 1, 2, or 3, it should continue the execution of the original method. If not, it should stop the execution with an error.
  11. Apply the OneTwoThree decorator to the byPoints parameter of updateScore and apply the Validate decorator to the updateScore method:

    Create a game object, and update its score a few times. The console should reflect the applications of all decoratorsas shown:

    [AUDIT] Updated score (updateScore) called with arguments:

    [AUDIT] [ 3, true ]

    [AUDIT] and returned result:

    [AUDIT] undefined

    //…

    [AUDIT] Updated score (updateScore) called with arguments:

    [AUDIT] [ 2, true ]

    [AUDIT] and returned result:

    [AUDIT] undefined

    [AUDIT] Updated score (updateScore) called with arguments:

    [AUDIT] [ 2, false ]

    [AUDIT] and returned result:

    [AUDIT] undefined

    7:8

    Note

    For ease of presentation, only a section of the expected output is shown here. The solution to this activity can be found via this link.

In this activity, you are leveraging decoration to quickly and efficiently implement complicated cross-cutting concerns. When you have successfully completed the activity, you will have implemented multiple kinds of decorators, according to the needs of the application, and thus will have widened the functionalities of your code without sacrificing clarity and readability.

Summary

In this chapter, you looked at a technique called decorating that is natively supported in TypeScript. The chapter first established the motivation for the use of decorators and then looked at the multiple types of decorators in TypeScript (class, method, accessor, property, and parameter decorators), along with examining the possibilities of each. You learned how to swap or change the complete constructor of a class with a class decorator, how to wrap a single method or property accessor with a method decorator, and how to enrich the available metadata using property and parameter decorators.

The chapter also discussed the differences between active and passive decorators, which boil down to a difference between code and definition. You implemented several common variants of each of the decorator types and demonstrated how different decorator types can nicely complement each other. This chapter should help you easily manage the usage and creation of decorators both from third-party libraries such as Angular and from decorator factories created by yourself. In the next chapter, we will begin our foray into dependency injection in TypeScript.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset