📜 ⬆️ ⬇️

Decorators and reflection in TypeScript: from beginner to expert (Part 1)



From the translator: TypeScript is a rather young and actively developing language. Unfortunately, there is quite a bit of information about him in the Russian part of the Internet, which does not contribute to its popularity.

Many of the features that are now implemented in ES6, appeared much earlier in TypeScript. Moreover, some of the capabilities and proposed ES7 standards also have experimental implementations in this language. About one of them, which appeared relatively recently - decorators - and will be discussed.
')
I bring to your attention a translation of the article (or rather, a series of articles) about decorators in TypeScript under the authorship of Remo H.Jansen



Not so long ago, Microsoft and Google announced a joint work on TypeScript and Angular 2.0.

We are pleased to announce that we are combining TypeScript and AtScript languages, and also that Angular 2, the next version of the popular JavaScript library for creating websites and web applications, will be developed in TypeScript





Annotations and decorators



This collaboration helped TypeScript to develop new language features, among which we will highlight annotations .

Annotations are a way to add metadata to a class declaration for use in dependency injection or compiler directives.



Annotations were suggested by the AtScript team from Google, but they are not standard. In the meantime, decorators have been proposed as a standard in ECMAScript 7 for changing classes and properties at the design stage.

Annotations and decorators are very similar:

Annotations and decorators are about the same. From the user's point of view, they have exactly the same syntax. The difference is that we do not control how annotations add metadata to our code. We can view decorators as an interface for building something that behaves like annotations.

In the long term, however, we will focus on decorators, since they are the existing proposed standard. AtScript is TypeScript, and TypeScript implements decorators.



Let's take a look at the TypeScript decorator syntax.

TypeScript Decorators



In the TypeScript source code, you can find the signatures of the available types of decorators:

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void; declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void; declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void; declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void; 

As you can see, they can be used to annotate a class , property , method, or parameter . Let's take a closer look at each of these types.

Method Decorators



Now that we know what the decorators signature looks like, we can try to implement them. Let's start with the method decorator . Let's create a method decorator called "log".

To call the decorator before declaring a method, you need to write the character "@", and then the name of the decorator used. In case the decorator is called "log", this syntax looks like this:

 class C { @log foo(n: number) { return n * 2; } } 

Before using @log , you must declare the decorator itself somewhere in our application. Let's look at its implementation:

 function log(target: Function, key: string, value: any) { return { value: function (...args: any[]) { var a = args.map(a => JSON.stringify(a)).join(); var result = value.value.apply(this, args); var r = JSON.stringify(result); console.log(`Call: ${key}(${a}) => ${r}`); return result; } }; } 

The method decorator takes 3 arguments:



A bit strange, yes? We did not pass any of these parameters when we used the @log decorator in the class C declaration. In this regard, two questions arise: who is passing these arguments? and where exactly is the log method called?

Answers to them can be found by looking at the code that generates TypeScript for the example above.

  var C = (function () { function C() { } C.prototype.foo = function (n) { return n * 2; }; Object.defineProperty(C.prototype, "foo", __decorate([ log ], C.prototype, "foo", Object.getOwnPropertyDescriptor(C.prototype, "foo"))); return C; })(); 

Without the @log decorator @log generated Javascript code for class C would look like this:

  var C = (function () { function C() { } C.prototype.foo = function (n) { return n * 2; }; return C; })(); 

As you can see, adding @log following code to the class definition with the TypeScript compiler:

 Object.defineProperty(C.prototype, "foo", __decorate( [log], // decorators C.prototype, // target "foo", // key Object.getOwnPropertyDescriptor(C.prototype, "foo") // desc ); ); 

If we refer to the documentation , we will learn the following about the defineProperty method:

The Object.defineProperty() method detects a new or changes an existing property directly on an object and returns this object.



The TypeScript compiler passes the prototype C , the name of the method to be decorated ('foo'), and the result of the __decorate function to the __decorate method.

defineProperty used to override the method being decorated. The new implementation of the method is the result of the function __decorate . A new question arises: where is the __decorate function __decorate ?

If you previously worked with TypeScript, you might notice that when we use the extends , a function called __extends generated by the compiler.

 var __extends = this.__extends || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } __.prototype = b.prototype; d.prototype = new __(); }; 

Similarly, when we use a decorator, a function called __decorate generated by the TypeScript compiler. Let's take a look at it.

 var __decorate = this.__decorate || function (decorators, target, key, desc) { if (typeof Reflect === "object" && typeof Reflect.decorate === "function") { return Reflect.decorate(decorators, target, key, desc); } switch (arguments.length) { case 2: return decorators.reduceRight(function(o, d) { return (d && d(o)) || o; }, target); case 3: return decorators.reduceRight(function(o, d) { return (d && d(target, key)), void 0; }, void 0); case 4: return decorators.reduceRight(function(o, d) { return (d && d(target, key, o)) || o; }, desc); } }; 

The first line in this listing uses the OR operator to make sure that the __decorate function generated more than once will not be overwritten again and again. In the second line we can notice the condition:

 if (typeof Reflect === "object" && typeof Reflect.decorate === "function") 

This condition is used to detect the future possibility of the JavaScript metadata reflection API . We will take a closer look at it closer to the end of this series of articles.

Let's stop for a second and remember how we came to this point. The foo method is overridden by the result of the __decorate function, which is called with the following parameters:

 __decorate( [log], // decorators C.prototype, // target "foo", // key Object.getOwnPropertyDescriptor(C.prototype, "foo") // desc ); 

Now we are inside the __decorate function and, since the metadata reflection API is not available, the version generated by the compiler will be executed

 // arguments.length ===  ,   __decorate() switch (arguments.length) { case 2: return decorators.reduceRight(function(o, d) { return (d && d(o)) || o; }, target); case 3: return decorators.reduceRight(function(o, d) { return (d && d(target, key)), void 0; }, void 0); case 4: return decorators.reduceRight(function(o, d) { return (d && d(target, key, o)) || o; }, desc); } 

Since in our case 4 arguments were passed to the __decorate method, the last option will be selected. Dealing with this code is not so easy because of the meaningless names of variables, but we are not afraid, are we?

Let's first learn what the reduceRight method does.

reduceRight applies the accumulating function to each element of the array (in order from right to left) and returns a single value.



The code below performs exactly the same operation, but is rewritten for ease of understanding:

  [log].reduceRight(function(log, desc) { if(log) { return log(C.prototype, "foo", desc); } else { return desc; } }, Object.getOwnPropertyDescriptor(C.prototype, "foo")); 

When this code is executed, the log decorator is called and we see the parameters passed to it: C.prototype , "foo" and previousValue . That is, now we know the answers to our questions:



After decorating, the foo method will continue to work as usual, but its calls will also run additional logging functionality added in the log decorator.

 var c = new C(); var r = c.foo(23); // "Call: foo(23) => 46" console.log(r); // 46 


findings



Not a bad adventure, huh? I hope you enjoyed it as much as I did. We’ve just started, and we’ve already learned how to do some really cool things.

Method decorators can be used for various interesting "chips". For example, if you worked with "" (spy ) in test frameworks like SinonJS , you might be delighted to use decorators to create "spies" simply by adding the @spy decorator.

In the next part, we will learn how to work with property decorators .

Source: https://habr.com/ru/post/275003/


All Articles