This article is the third part of the series:
Last time, we learned what decorators are and how they are implemented in TypeScript. We know how to work with decorators of classes, properties and methods.
In this article we will tell about:
We will use the following class to demonstrate these concepts:
class Person { public name: string; public surname: string; constructor(name : string, surname : string) { this.name = name; this.surname = surname; } public saySomething(something : string) : string { return this.name + " " + this.surname + " says: " + something; } }
As we already know, the parameter decorator signature looks like this:
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
Using a decorator called logParameter
will look like this:
class Person { public name: string; public surname: string; constructor(name : string, surname : string) { this.name = name; this.surname = surname; } public saySomething(@logParameter something : string) : string { return this.name + " " + this.surname + " says: " + something; } }
When compiling in JavaScript, the __decorate
method is called here (we talked about it in the first part ).
Object.defineProperty(Person.prototype, "saySomething", __decorate([ __param(0, logParameter) ], Person.prototype, "saySomething", Object.getOwnPropertyDescriptor(Person.prototype, "saySomething"))); return Person;
By analogy with the previous types of decorators, we can assume that once the Object.defineProperty
method is Object.defineProperty
, the saySomething
method will be replaced by the result of calling the __decorate
function (as in the method decorator). This assumption is incorrect.
If you look closely at the code above, you will notice that there is a new __param
function __param
. It was generated by the TypeScript compiler and looks like this:
var __param = this.__param || function (index, decorator) { // return a decorator function (wrapper) return function (target, key) { // apply decorator (return is ignored) decorator(target, key, index); } };
The __param
function returns a decorator that wraps the decorator passed to the input (named decorator
).
You may notice that when the parameter decorator is called, its value is ignored. This means that when calling the function __decorate
, the result of its execution will not override the saySomething
method.
Therefore, the parameter decorators do not return anything .
__param
decorator in __param
used to save the index (the position of the parameter to decorate in the argument list) in the closure.
class foo { // foo index === 0 public foo(@logParameter foo: string) : string { return "bar"; } // bar index === 1 public foobar(foo: string, @logParameter bar: string) : string { return "foobar"; } }
Now we know that the parameter decorator takes 3 arguments:
Let's implement logProperty
function logParameter(target: any, key : string, index : number) { var metadataKey = `log_${key}_parameters`; if (Array.isArray(target[metadataKey])) { target[metadataKey].push(index); } else { target[metadataKey] = [index]; } }
The parameter decorator, described above, adds a new property ( metadataKey
) to the class prototype. This property is an array containing the indices of the parameters to be decorated. We can consider this property as metadata .
It is assumed that the parameter decorator is not used to modify the behavior of a constructor, method, or property. Parameter decorators should only be used to create various metadata .
Once the metadata is created, we can use another decorator to read it. For example, the following is a modified version of the method decorator from the second part of the article .
The original version output to the console the name of the method and all its arguments when called. The new version reads metadata , and on their basis displays only those arguments that are marked with the corresponding parameter decorator.
class Person { public name: string; public surname: string; constructor(name : string, surname : string) { this.name = name; this.surname = surname; } @logMethod public saySomething(@logParameter something : string) : string { return this.name + " " + this.surname + " says: " + something; } } function logMethod(target: Function, key: string, descriptor: any) { var originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { var metadataKey = `__log_${key}_parameters`; var indices = target[metadataKey]; if (Array.isArray(indices)) { for (var i = 0; i < args.length; i++) { if (indices.indexOf(i) !== -1) { var arg = args[i]; var argStr = JSON.stringify(arg) || arg.toString(); console.log(`${key} arg[${i}]: ${argStr}`); } } var result = originalMethod.apply(this, args); return result; } else { var a = args.map(a => (JSON.stringify(a) || a.toString())).join(); var result = originalMethod.apply(this, args); var r = JSON.stringify(result); console.log(`Call: ${key}(${a}) => ${r}`); return result; } } return descriptor; }
In the next section, we will learn the best way to work with metadata: Metadata Reflection API . Here is a small example of what we will learn:
function logParameter(target: any, key: string, index: number) { var indices = Reflect.getMetadata(`log_${key}_parameters`, target, key) || []; indices.push(index); Reflect.defineMetadata(`log_${key}_parameters`, indices, target, key); }
The official proposal of decorators in TypeScript gives the following definition of a factory of decorators:
A decorator factory is a function that can take any number of arguments and returns one of the decorator types.
We have already learned how to implement and use all types of decorators (class, method, property, and parameter), but we can improve something. Suppose we have this code snippet:
@logClass class Person { @logProperty public name: string; public surname: string; constructor(name : string, surname : string) { this.name = name; this.surname = surname; } @logMethod public saySomething(@logParameter something : string) : string { return this.name + " " + this.surname + " says: " + something; } }
It works as it should, but it would be better if the same decorator could be used everywhere without worrying about its type , as in this example:
@log class Person { @log public name: string; public surname: string; constructor(name : string, surname : string) { this.name = name; this.surname = surname; } @log public saySomething(@log something : string) : string { return this.name + " " + this.surname + " says: " + something; } }
We can achieve this by wrapping decorators in a factory. A factory can determine the type of decorator needed by the arguments passed to it:
function log(...args : any[]) { switch(args.length) { case 1: return logClass.apply(this, args); case 2: return logProperty.apply(this, args); case 3: if(typeof args[2] === "number") { return logParameter.apply(this, args); } return logMethod.apply(this, args); default: throw new Error("Decorators are not valid here!"); } }
The last point that I would like to discuss in this article is how we can pass arguments to the decorator when using it .
@logClassWithArgs({ when : { name : "remo"} }) class Person { public name: string; // ... }
We can use the decorator factory to create configurable decorators:
function logClassWithArgs(filter: Object) { return (target: Object) => { // , // (filter), // } }
We can apply the same idea for other types of decorators.
Now we have a deep understanding of all four existing types of decorators, how to create decorator factories and how to parameterize them.
In the next article we will learn how to use the Metadata Reflection API .
Source: https://habr.com/ru/post/303516/
All Articles