Study Note with Decorator In TypeScript (1) - Property Decorator

Inroduction

If you have ever used Angular, you must have used Decorators to annotate your classes (@service, @component, etc…) and methods (@input, @output, etc…). Besides, when I was learning React, I used MobX for state management, in which Decorators are also heavily used (@observer, @action, @computed, etc…)

The purpose of this post is to give a brief introduction about how to implement a very simple and basic decorator, assigning values to properties (Of course you can directly assign the value to the property. In here, I just demonstrate how a decorator can do the same thing).

Concepts of Decorator

According to the definition from TypeScript documentation

With the introduction of Classes in TypeScript and ES6, there now exist certain scenarios that require additional features to support annotating or modifying classes and class members. 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.

In my understanding, Decorator is just a special function. It accepts a function / property as an input parameter and output a function / parameter.

Environment Setup

  1. Install TypeScript in your machine by running the following command

    $ npm install -g typescript

    It will also install TypeScript compiler globally. And then you can run tsc in the command line to compile TypeScript files into JavaScript.

  2. Create package.json file using by running the following command

    $ npm init

    After creation of the file, add the following changes to package.json file.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    "name": "decorator-test",
    "version": "1.0.0",
    "description": "",
    "main": "dist/index.js", // Configure the entry file to dist/index.js
    "scripts": {
    "start": "tsc && node dist/index.js" // Configure start command here
    },
    "author": "",
    "license": "ISC",
    "devDependencies": {}
    }
  3. Create tsconfig.json file to specify the configuration of TypeScript for this project.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    "compilerOptions": {
    "pretty": true,
    "target": "es5",
    "outDir": "dist", // Specify the directory where the compiled js files are generated
    "emitDecoratorMetadata": true, // To allow emit decorator meta data
    "experimentalDecorators": true // To allow to use Decorators
    },
    "include": ["src/**/*.ts"],
    "exclude": ["node_modules", "src/**/*.test.ts"]
    }
  4. Create src/index.ts file to test the correctness of configuration.

    1
    console.log('test');
  5. Run npm start.

    If you can see a dist folder with index.js is created, and the console correctly print test, your configuration is correct.

Now you may have the following file directory after completing the setup.

1
2
3
4
5
6
7
8
.
├── dist
│ └── index.js
├── src
│ └── index.ts
├── package-lock.json
├── package.json
└── tsconfig.json

Write A Property Decorator

We can take the man wearing a jacket as an example of implementing decorators. Let’s clear and add the following code to index.ts file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
public wearing: string = null; // What is the person wearing

/**
* Ask what the person is wearing, and you will get answered
*/
public askWearing() {
if (this.wearing) {
console.log(`I am wearing a ${this.wearing}`);
} else {
console.log(`I am wearing nothing`);
}
}
}

const zhiyue = new Person();

zhiyue.askWearing(); // I am wearing nothing

A class Person is created with a string property wearing and a method, askWearing(). Then an instance of Person is created and it’s method is called.

Since we have not set the property wearing, the method will just print I am wearing nothing.

Since wearing nothing in public areas is illegal in Singapore (LOL), it is better to let the person wear something at the time the instance is created.

In this case, we try to use a decorator to achieve our purpose.

Add the following decorator to the code.

1
2
3
4
5
6
7
function Wear(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
get: function() {
return 'Jacket';
}
});
}

And then add this decorator to the Person class.

1
2
3
4
5
class Person {
@Wear
public wearing: string;
// ...
}

Now, run the code, the console will print I am wearing a Jacket.

But what if we want to let the Jacket be passed to the property as a parameter? Something like

1
2
3
4
5
class Person {
@Wear('Jacket')
public wearing: string;
// ...
}

Then we have to modify our decorator with a function as a wrapper to get the parameter.

1
2
3
4
5
6
7
8
9
function wear(wearing: string) {
return function(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
get: function() {
return wearing;
}
});
};
}

Now it accept Jacket as a string parameter, which is changeable. Run the code and you will get I am wearing a Jacket in the console.

The full version of index.ts is here:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Person {
@wear('Jacket')
public wearing: string; // What is the person wearing

/**
* Ask what the person is wearing, and you will get answered
*/
public askWearing() {
if (this.wearing) {
console.log(`I am wearing a ${this.wearing}`);
} else {
console.log(`I am wearing nothing`);
}
}
}

function wear(wearing: string) {
return function(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
get: function() {
return wearing;
}
});
};
}

const zhiyue = new Person();

zhiyue.askWearing(); // I am wearing a Jacket

Conclusion

Demo in GitHub Repo

In reality, we can do a lot more complexed things with decorators. In fact, MobX is a very good library to study about decorator. It has numbers of easy-to-use decorators. As a React novice, MobX makes me feel enjoyable doing state management in React.

I am also new to Decorator and there are still so many things for me to learn. I will update my progress in the next post!

Related