Decorators are incredibly awesome. They allow you to describe meta information directly in the class declaration, grouping everything in one place and avoiding duplication. Terribly convenient. Once having tried, you will never agree to write in the old way.
However, despite all the usefulness, the decorators in TypeScript (also declared on the standard) are not as simple as we would like. Working with them requires Jedi skills, since it is necessary to understand the object model of JavaScript (well, you understand what I mean), the API is somewhat confusing and, moreover, not yet stable. In this article I will talk a little about the device decorators and show a few specific techniques, how to put this dark force for the benefit of front-end development.
Besides TypeScript, decorators are available in Babel . This article only covers implementation in TypeScript.
You can decorate in TypeScript classes, methods, method parameters, accessor properties (accessors) and fields.
In TypeScript, the term "field" is not commonly used, and the fields are also called properties . This creates a lot of confusion because there is a difference. If we declare a property with get / set access methods, then a Object.defineProperty call appears in the class declaration and a descriptor is available in the decorator, and if we declare just a field (in C # and Java terms), then nothing appears , and, accordingly, transferred to the decorator. This defines the decorators signature, so I use the term "field" to distinguish them from properties with access methods.
In general, a decorator is an expression , preceded by the "@" symbol, which returns a function of a certain type ( different in each case ). Actually, you can simply declare such a function and use its name as a decorator expression:
function MyDecorator(target, propertyKey, descriptor) { // ... } class MyClass { @MyDecorator myMethod() { } }
However, you can use any other expression that returns such a function. For example, you can declare another function that will accept additional information as parameters and return the corresponding lambda. Then, as the decorator, we will use the expression " function call MyAdvancedDecorator ".
function MyAdvancedDecorator(info?: string) { return (target, propertyKey, descriptor) => { // .. }; } class MyClass { @MyAdvancedDecorator("advanced info") myMethod() { } }
Here, the most common function call, so even if we do not pass parameters, you still need to write the brackets "@MyAdvancedDecorator ()". Actually, these are the two main ways to advertise decorators.
During the compilation process, the decorator declaration results in a call to our function in the class definition. That is, where Object.defineProperty
is Object.defineProperty
, the class prototype is filled in and all that. How exactly this happens is important to know, because This explains when the decorator is called, what the parameters of our function are, why they are just like that, and what and how you can do in the decorator. Below is the simplified code that compiles our class with the decorator:
var __decorateMethod = function (decorators, target, key) { var descriptor = Object.getOwnPropertyDescriptor(target, key); for (var i = decorators.length - 1; i >= 0; i--) { var decorator = decorators[i]; descriptor = decorator(target, key, descriptor) || descriptor; // } Object.defineProperty(target, key, descriptor); }; // MyClass var MyClass = (function () { function MyClass() {} // MyClass.prototype.myMethod = function () { }; // myMethod // __decorateMethod([ MyAdvancedDecorator("advanced info") // , ], MyClass.prototype, "myMethod"); return MyClass; }());
The table below provides a description of the function for each type of decorators, as well as links to examples in the TypeScript Playground , where you can see what exactly the decorators are compiled in and try them in action.
View decorator | Function signature |
---|---|
Class decorator Playground example @MyDecorator class MyClass {} | function MyDecorator <TFunction extends Function> (target: TFunction): TFunction { return target; }
|
Method decorator Playground example ') class MyClass { @MyDecorator myMethod () {} } | function MyDecorator (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor <any>): TypedPropertyDescriptor <any> { return descriptor; }
|
Static method decorator Playground example class MyClass { @MyDecorator static myMethod () {} } | function MyDecorator (target: Function, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor <any>): TypedPropertyDescriptor <any> { return descriptor; }
|
Accessor decorator Playground example class MyClass { @MyDecorator get myProperty () {} } | Similar to the method. The decorator should be applied to the first access method (get or set), in the order of declaration in the class. |
Parameter decorator Playground example class MyClass { myMethod ( @MyDecorator val) { } } | function MyDecorator (target: Object, propertyKey: string | symbol, index: number): void {}
|
Field decorator (properties) Playground example class MyClass { @MyDecorator myField: number; } | function MyDecorator (target: Object, propertyKey: string | symbol): TypedPropertyDescriptor <any> { return null; }
|
Static field decorator (properties) Playground example class MyClass { @MyDecorator static myField; } | function MyDecorator (target: Function, propertyKey: string | symbol): TypedPropertyDescriptor <any> { return null; }
|
Interfaces | Interface decorators and their members are not supported. |
Type declarations | Decorators are not supported in ambient declarations. |
Functions and variables outside the class | Decorators outside the class are not supported. |
The TypedPropertyDescriptor <T> interface appearing in the signature of method and property decorators is declared as follows:
interface TypedPropertyDescriptor<T> { enumerable?: boolean; configurable?: boolean; writable?: boolean; value?: T; get?: () => T; set?: (value: T) => void; }
If you specify a specific type T for a TypedPropertyDescriptor in the decorator’s declaration, you can limit the type of properties to which the decorator is applicable. What the members of this interface mean is here . In short, for the value method it contains the method itself, for the field - the value, for the property - get and set contain the corresponding access methods.
Decorator support is experimental and may change in future releases (in TypeScript 2.0 it has not changed). Therefore, you must add experimentalDecorators: true to tsconfig.json. In addition, decorators are only available if target: es5 or higher.
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true } }
Important!!! - About target: ES3 and JSFiddle
It is important not to forget to specify the target - ES5 option when working with decorators. If this is not done, the code will compile without errors, but it will work differently ( this is a bug in the TypeScript compiler ). In particular, the third parameter will not be passed to the decorators of methods and properties, and their return value will be ignored.
These phenomena can be observed in JSFiddle (this is already a bug in JSFiddle ), so in this article I don’t place examples in JSFiddle.
However, there is a workaround for these bugs. You just need to get the descriptor yourself, and update it yourself. For example, here is the implementation of the @safe decorator , which works with both target ES3 and ES5.
To use type information, you must also add emitDecoratorMetadata: true.
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true, "emitDecoratorMetadata": true } }
To use the Reflect class, you need to install the additional package reflect-metadata :
npm install reflect-metadata --save
And in the code:
import "reflect-metadata";
However, if you use Angular 2, then your build system may already contain the Reflect implementation, and after installing the reflect-metadata package, you can get the runtime error Unexpected value 'YourComponent' exported by the module 'YourModule'
. In this case, it is better to install only typings .
typings install dt~reflect-metadata --global --save
So, let's move on to practice. Consider a few examples that demonstrate the capabilities of decorators.
Suppose we often have minor functions, the errors within which we would like to ignore. Every time try / catch is cumbersome, the decorator comes to the rescue:
function safe(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> { // var originalMethod = descriptor.value; // descriptor.value = function SafeWrapper () { try { // originalMethod.apply(this, arguments); } catch(ex) { // , console.error(ex); } }; // return descriptor; }
class MyClass { @safe public foo(str: string): boolean { return str.length > 0; // str == null, } } var test = new MyClass(); console.info("Starting..."); test.foo(null); console.info("Continue execution");
Result of performance:
Try it in action in Plunker
View in Playground
For example, if you change the value of a field, you need to perform some kind of logic. You can, of course, define a property with get / set methods, and put the necessary code in the set. And you can reduce the amount of code by declaring the decorator:
function OnChange<ClassT, T>(callback: (ClassT, T) => void): any { return (target: Object, propertyKey: string | symbol) => { // , . // . var descriptor = Object.getOwnPropertyDescriptor(target, propertyKey) || {configurable: true, enumerable: true}; // get set var value: T; var originalGet = descriptor.get || (() => value); var originalSet = descriptor.set || (val => value = val); descriptor.get = originalGet; descriptor.set = function(newVal: T) { // , set function, // this - , // , this - Window!!! var currentVal = originalGet.call(this); if (newVal != currentVal) { // callback callback.call(target.constructor, this, newVal); } originalSet.call(this, newVal); }; // , Object.defineProperty(target, propertyKey, descriptor); return descriptor; } }
Notice that we call defineProperty and return the handle from the decorator. This is due to a bug in reflect-metadata , due to which the return value is ignored for the field decorator.
class MyClass { @OnChange(MyClass.onFieldChange) public mMyField: number = 42; static onFieldChange(self: MyClass, newVal: number): void { console.info("Changing from " + self.mMyField + " to " + newVal); } } var test = new MyClass(); test.mMyField = 43; test.mMyField = 44;
Result of performance:
» Try it in action in Plunker
» View in the Playground
We had to declare the handler as static, because it is difficult to sue on the instance method. Here is an alternative option with a string parameter , and another using lambda .
One of the interesting features of decorators is the ability to receive information about the type of the property or parameter being decorated (say “thanks” Angular, because it was done specifically for it). For this to work, you need to include the reflect-metadata library, and enable the emitDecoratorMetadata option (see above). After that, for properties that have at least one decorator, you can call Reflect.getMetadata with the key "design: type", and get a constructor of the corresponding type. Below is a simple implementation of the @Inject
decorator, which uses this technique to @Inject
dependencies:
// function Inject(target: Object, propKey: string): any { // // ( ILogService) var propType = Reflect.getMetadata("design:type", target, propKey); // var descriptor = { get: function () { // this - var serviceLocator = this.serviceLocator || globalSericeLocator; return serviceLocator.getService(propType); } }; Object.defineProperty(target, propKey, descriptor); return descriptor; }
Notice that we call defineProperty and return the handle from the decorator. This is due to a bug in reflect-metadata , due to which the return value is ignored for the field decorator.
// , , abstract class ILogService { abstract log(msg: string): void; } class Console1LogService extends ILogService { log(msg: string) { console.info(msg); } } class Console2LogService extends ILogService { log(msg: string) { console.warn(msg); } } var globalSericeLocator = new ServiceLocator(); globalSericeLocator.registerService(ILogService, new ConsoleLogService1()); class MyClass { @Inject private logService: ILogService; sayHello() { this.logService.log("Hello there"); } } var my = new MyClass(); my.sayHello(); my.serviceLocator = new ServiceLocator(); my.serviceLocator.registerService(ILogService, new ConsoleLogService2()); my.sayHello();
class ServiceLocator { services: [{interfaceType: Function, instance: Object }] = [] as any; registerService(interfaceType: Function, instance: Object) { var record = this.services.find(x => x.interfaceType == interfaceType); if (!record) { record = { interfaceType: interfaceType, instance: instance}; this.services.push(record); } else { record.instance = instance; } } getService(interfaceType: Function) { return this.services.find(x => x.interfaceType == interfaceType).instance; } }
As you can see, we simply declare the logService field, and the decorator already determines its type, and sets the access method that the corresponding service instance receives. Beautiful and comfortable. Result of performance:
» Try it in Plunker
» View in the Playground
Suppose for some reason you need to rename some object fields when serialized to JSON. With the help of a decorator, we will be able to declare a JSON-name of the field, and then, when serialized, read it. Technically, this decorator illustrates the work of the reflect-metadata library, and, in particular, the functions Reflect.defineMetadata and Reflect.getMetadata.
// const JsonNameMetadataKey = "Habrahabr_PFight77_JsonName"; // function JsonName(name: string) { return (target: Object, propertyKey: string) => { // name Reflect.defineMetadata(JsonNameMetadataKey, name, target, propertyKey); } } // , function serialize(model: Object): string { var result = {}; var target = Object.getPrototypeOf(model); for(var prop in model) { // var jsonName = Reflect.getMetadata(JsonNameMetadataKey, target, prop) || prop; result[jsonName] = model[prop]; } return JSON.stringify(result); }
class Model { @JsonName("name") public title: string; } var model = new Model(); model.title = "Hello there"; var json = serialize(model); console.info(JSON.stringify(moel)); console.info(json);
Result of performance:
» Try it in Plunker
» View in the Playground
This decorator has the disadvantage that if the model contains objects of other classes as fields, the fields of these classes are not processed by the serialize method (that is, the decorator @JsonName cannot be applied to them). In addition, the inverse transformation is not implemented here - from JSON to the client model. Both of these drawbacks are fixed in a somewhat more complex implementation of the server model converter, in the spoiler below.
@ServerModelField - server models converter on decorators
The statement of the problem is as follows. From the server, some JSON data of approximately the same type arrive to us (similar JSON sends one BaaS service):
{ "username":"PFight77", "email":"test@gmail.com", "doc": { "info":"The author of the article" } }
We want to convert this data into a typed object, renaming some fields. In the end, it will look like this:
class UserAdditionalInfo { @ServerModelField("info") public mRole: string; } class UserInfo { @ServerModelField("username") private mUserName: string; @ServerModelField("email") private mEmail: string; @ServerModelField("doc") private mAdditionalInfo: UserAdditionalInfo; public get DisplayName() { return mUserName + " " + mAdditionalInfo.mRole; } public get ID() { return mEmail; } public static parse(jsonData: string): UserInfo { return convertFromServer(JSON.parse(jsonData), UserInfo); } public serialize(): string { var serverData = convertToServer(this); return JSON.stringify(serverData); } }
Let us see how this is implemented.
First, we need to define a decoder for the ServerModelField field , which will take a string parameter and store it in metadata. In addition, to parse JSON, we still need to know what fields with our decorator are in the class at all. To do this, we will declare another instance of metadata, common to all fields of the class, in which we will save the names of all decorated members. Here we will not only save metadata via Relect.defineMetadata, but also receive via Reflect.getMetadata.
// , const ServerNameMetadataKey = "Habrahabr_PFight77_ServerName"; const AvailableFieldsMetadataKey = "Habrahabr_PFight77_AvailableFields"; // export function ServerModelField(name?: string) { return (target: Object, propertyKey: string) => { // name, , Reflect.defineMetadata(ServerNameMetadataKey, name || propertyKey, target, propertyKey); // , availableFields var availableFields = Reflect.getMetadata(AvailableFieldsMetadataKey, target); if (!availableFields) { // Ok, , availableFields = []; // 4- (propertyKey) defineMetadata, // .. Reflect.defineMetadata(AvailableFieldsMetadataKey, availableFields, target); } // availableFields.push(propertyKey); } }
Well, it remains to write the function convertFromServer. It is almost nothing special, it just calls Reflect.getMetadata and uses the resulting metadata to parse JSON. One feature is that this function should create an instance of UserInfo via new, so we are passing to it, in addition to JSON data, a class: convertFromServer(JSON.parse(data), UserInfo)
. To understand how this works, look at the spoiler below.
class MyClass { } // " " var myType: { new(): any; }; // myType = MyClass; // new MyClass() var obj = new myType();
The second feature is the use of field type data generated by setting up "emitDecoratorMetadata": true in tsconfig.json. The Reflect.getMetadata
is to call Reflect.getMetadata
with the key "design: type", which is returned by the constructor of the corresponding type. For example, a call to Reflect.getMetadata("design:type", target, "mAdditionalInfo")
will return the UserAdditionalInfo
constructor. We will use this information to properly process custom type fields. For example, the UserAdditionalInfo class also uses the @ServerModelField decorator, so we must also use this metadata for JSON parsing.
The third feature is to obtain the corresponding target, from where we will take the metadata. We use field decorators , so metadata needs to be taken from the class prototype. For static member decorators, use the class constructor. You can get a prototype by calling Object.getPrototypeOf or by calling the constructor's prototype property.
All other comments in the code:
export function convertFromServer<T>(serverObj: Object, type: { new(): T ;} ): T { // , , type var clientObj: T = new type(); // var target = Object.getPrototypeOf(clientObj); // , var availableNames = Reflect.getMetadata(AvailableFieldsMetadataKey, target) as [string]; if (availableNames) { // availableNames.forEach(propName => { // JSON var serverName = Reflect.getMetadata(ServerNameMetadataKey, target, propName); if (serverName) { // , var serverVal = serverObj[serverName]; if (serverVal) { var clientVal = null; // , @ServerModelField // var propType = Reflect.getMetadata("design:type", target, propName); // , var propTypeServerFields = Reflect.getMetadata(AvailableFieldsMetadataKey, propType.prototype) as [string]; if (propTypeServerFields) { // , , clientVal = convertFromServer(serverVal, propType); } else { // , clientVal = serverVal; } // clientObj[propName] = clientVal; } } }); } else { errorNoPropertiesFound(getTypeName(type)); } return clientObj; } function errorNoPropertiesFound<T>(typeName: string) { throw new Error("There is no @ServerModelField directives in type '" + typeName + "'. Nothing to convert."); } function getTypeName<T>(type: { new(): T ;}) { return parseTypeName(type.toString()); } function parseTypeName(ctorStr: string) { var matches = ctorStr.match(/\w+/g); if (matches.length > 1) { return matches[1]; } else { return "<can not determine type name>"; } }
The inverse function has a similar form - convertToServer.
function convertToServer<T>(clientObj: T): Object { var serverObj = {}; var target = Object.getPrototypeOf(clientObj); var availableNames = Reflect.getMetadata(AvailableFieldsMetadataKey, target) as [string]; availableNames.forEach(propName=> { var serverName = Reflect.getMetadata(ServerNameMetadataKey, target, propName); if (serverName) { var clientVal = clientObj[propName]; if (clientVal) { var serverVal = null; var propType = Reflect.getMetadata("design:type", target, propName); var propTypeServerFields = Reflect.getMetadata(AvailableFieldsMetadataKey, propType.prototype) as [string]; if (clientVal && propTypeServerFields) { serverVal = convertToServer(clientVal); } else { serverVal = clientVal; } serverObj[serverName] = serverVal; } } }); if (!availableNames) { errorNoPropertiesFound(parseTypeName(clientObj.constructor.toString())); } return serverObj; }
@ServerModelField plunker .
ASP.NET , , , . , url , /ControllerName/ActionName. , , . , ..
TypeScript, . , , url .
var ControllerNameMetadataKey = "Habr_PFight77_ControllerName"; // . // , // ( ), // . function Controller(name: string) { return (target: Function) { Reflect.defineMetadata(ControllerNameMetadataKey, name, target.prototype); }; } // , function Action(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> { // var originalMethod = descriptor.value; // descriptor.value = function ActionWrapper () { // url, Controller var controllerName = Reflect.getMetadata(ControllerNameMetadataKey, target); // url /ControllerName/ActionName var url = "/" + controllerName + "/" + propertyKey; // url [].push.call(arguments, url); // originalMethod.apply(this, arguments); }; // return descriptor; } // , function post(data: any, args: IArguments): any { // url, @Action var url = args[args.length - 1]; return $.ajax({ url: url, data: data, method: "POST" }); }
@Controller("Account") class AccountController { @Action public Login(data: any): any { return post(data, arguments); } } var Account = new AccountController(); Account.Login({ username: "user", password: "111"});
Result of performance:
» Plunker
» Playground
, TypeScript . JSON. , , ( , Controller ).
TypeScript , . - ++, .
, :
. , , , .. , .
Reflect. , , .
, . , 8 . ( ), . , API ReactJS , this .
That's all for now. , .
UPD. whileTrue , ES7 . , ES8 .
Source: https://habr.com/ru/post/310870/
All Articles