デコレータは非常にクールです。 クラス宣言でメタ情報を直接記述し、すべてを1か所にグループ化し、重複を避けることができます。 とても快適。 一度試してみると、古い方法で書くことに同意することはありません。
ただし、すべての有用性にもかかわらず、TypeScriptデコレータ(標準としても宣言されています)は、私たちが望むほど単純ではありません。 JavaScriptオブジェクトモデルを理解する必要があるため(つまり、私が言っていることを理解しているため)、Jediのスキルが必要になります 。 この記事では、デコレータのデバイスについて少しお話しし、フロントエンド開発の利益のためにこの暗黒の力をどのようにかけるかについて、いくつかの具体的なトリックを示します。
TypeScriptに加えて、Babelでデコレータを利用できます 。 この記事では、TypeScriptでの実装についてのみ説明します。

基本
TypeScriptは、クラス、メソッド、メソッドパラメーター、アクセサー、およびフィールドで装飾できます。
TypeScriptでは、「フィールド」という用語は一般的に使用されず、フィールドはプロパティとも呼ばれます 。 これにより、多くの混乱が生じます。 違いがあります。 get / setアクセスメソッドでプロパティを宣言すると、Object.definePropertyの呼び出しがクラス宣言に表示され、記述子がデコレータで使用可能になり、フィールドを(C#とJavaの観点から)単に宣言すると、 何も表示されず、したがって、記述子は表示されませんデコレータに渡されます。 これにより、デコレータのシグネチャが定義されるため、「フィールド」という用語を使用して、アクセスメソッドを持つプロパティと区別します。
一般に、デコレーターは@記号が前に付いた式であり、特定のタイプの関数( それぞれの場合で異なる )を返します。 実際、 そのような関数を単純に宣言し、 その名前をデコレーターの式として使用できます。
function MyDecorator(target, propertyKey, descriptor) { // ... } class MyClass { @MyDecorator myMethod() { } }
ただし、このような関数を返す他の式を使用できます。 たとえば、追加情報をパラメーターとして受け取り、対応するラムダを返す別の関数を宣言できます。 次に、デコレーターとして、「 MyAdvancedDecorator関数の呼び出し 」という式を使用します。
function MyAdvancedDecorator(info?: string) { return (target, propertyKey, descriptor) => { // .. }; } class MyClass { @MyAdvancedDecorator("advanced info") myMethod() { } }
これは最も一般的な関数呼び出しです。そのため、パラメーターを渡さなくても、括弧「@MyAdvancedDecorator()」を記述する必要があります。 実際、これらはデコレータを宣言する2つの主な方法です。
コンパイル中に、デコレータの宣言により、クラス定義で関数の呼び出しが表示されます。 つまり、 Object.defineProperty
がObject.defineProperty
、クラスのプロトタイプが生成されます。 これがどのように発生するかを知ることは重要です。 これは、デコレータがいつ呼び出されるか、関数のパラメータは何であるか、なぜそれらが正確に似ているのか、デコレータで何をどのように行うのかを説明します。 以下は、デコレータを使用してクラスをコンパイルする単純化されたコードです。
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; }());
次の表には、各デコレータのタイプの関数の説明と、 TypeScript Playgroundの例へのリンクが含まれています。ここでは、どのデコレータが正確にコンパイルされ、実際に動作するかを確認できます。
デコレータビュー | 関数シグネチャ |
---|---|
クラスデコレータ
遊び場での例 @MyDecorator クラスMyClass {} | function MyDecorator <TFunction extends Function>(ターゲット:TFunction):TFunction {
ターゲットを返す; }
|
メソッドデコレータ
遊び場での例 クラスMyClass { @MyDecorator myMethod(){} } | 関数 MyDecorator(ターゲット:オブジェクト、propertyKey:文字列|シンボル、記述子:TypedPropertyDescriptor <any>):TypedPropertyDescriptor <any> {
記述子を返します。 } |
静的メソッドデコレータ
遊び場での例 クラスMyClass { @MyDecorator 静的myMethod(){} } | 関数 MyDecorator(ターゲット:Function、propertyKey:文字列|シンボル、記述子:TypedPropertyDescriptor <any>):TypedPropertyDescriptor <any> {
記述子を返します。 }
|
アクセス方法デコレータ
遊び場の例 クラスMyClass { @MyDecorator get myProperty(){} } | メソッドに似ています。 デコレータは、クラスでの宣言の順序で、最初のアクセスメソッド(getまたはset)に適用する必要があります。
|
パラメータデコレータ
遊び場の例 クラスMyClass { myMethod( @MyDecorator val){ } } | 関数 MyDecorator(ターゲット:オブジェクト、propertyKey:文字列|シンボル、インデックス:数値):void {}
|
フィールドデコレータ(プロパティ) 遊び場での例 クラスMyClass { @MyDecorator myField:数値; } | 関数 MyDecorator(ターゲット:オブジェクト、propertyKey:文字列|シンボル):TypedPropertyDescriptor <any> {
nullを返します。 }
|
静的フィールドデコレータ(プロパティ)
遊び場での例 クラスMyClass { @MyDecorator 静的myField; } | 関数 MyDecorator(ターゲット:関数、propertyKey:文字列|シンボル):TypedPropertyDescriptor <any> {
nullを返します。 }
|
インターフェース | インターフェイスデコレータとそのメンバーはサポートされていません。
|
型宣言 | 型宣言(アンビエント宣言)のデコレータはサポートされていません。
|
クラス外の関数と変数
| クラス外のデコレータはサポートされていません。
|
メソッドおよびプロパティデコレータのシグネチャに表示されるTypedPropertyDescriptor <T>インターフェイスは、次のように宣言されます。
interface TypedPropertyDescriptor<T> { enumerable?: boolean; configurable?: boolean; writable?: boolean; value?: T; get?: () => T; set?: (value: T) => void; }
デコレータの宣言でTypedPropertyDescriptorに特定のTタイプを指定すると、デコレータが適用されるプロパティのタイプを制限できます。 このインターフェイスのメンバーの意味はここにあります 。 要するに、値メソッドの場合は、メソッド自体が含まれ、フィールドの場合は-値の場合、プロパティの場合は-対応するアクセスメソッドが含まれます。
環境設定
デコレータのサポートは実験的であり、将来のリリースで変更される可能性があります( TypeScript 2.0では変更されません)。 したがって、tsconfig.jsonにExperimentalDecorators:trueを追加する必要があります。 さらに、ターゲットがes5以上の場合にのみ、デコレーターを使用できます。
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true } }
重要な!!! - ターゲットについて:ES3およびJSFiddle
デコレータを使用するときは、ターゲットオプション-ES5を指定することを忘れないでください。 これを行わないと、コードはエラーなしでコンパイルされますが、動作は異なります( これはTypeScriptコンパイラのバグです )。 特に、3番目のパラメーターはメソッドとプロパティのデコレーターに渡されず、戻り値は無視されます。
これらの現象はJSFiddleで確認できます(これはすでにJSFiddleのバグです )。この記事では、JSFiddleに例を投稿しません。
ただし、これらのバグには回避策があります。 自分でハンドルを取得し、自分で更新する必要があります。 たとえば、ターゲットES3とES5の両方で動作する@safeデコレータの実装は次のとおりです。
タイプ情報を使用するには、emitDecoratorMetadata:trueも追加する必要があります。
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true, "emitDecoratorMetadata": true } }
Reflectクラスを使用するには、オプションのreflect-metadataパッケージをインストールする必要があります。
npm install reflect-metadata --save
そしてコードでは:
import "reflect-metadata";
ただし、Angular 2を使用している場合、ビルドシステムには既にReflect実装が含まれている可能性があり、reflect-metadataパッケージをインストールした後Unexpected value 'YourComponent' exported by the module 'YourModule'
たランタイムエラーUnexpected value 'YourComponent' exported by the module 'YourModule'
。 この場合は、 typingsのみを設定することをお勧めします。
typings install dt~reflect-metadata --global --save
それでは、練習に移りましょう。 デコレータの力を示すいくつかの例を見てみましょう。
safe- 関数内の自動エラー処理

エラーを無視したい二次機能に頻繁に遭遇するとします。 毎回try / catchを書くのは面倒で、デコレーターが助けになります:
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");
実行結果:
@OnChange- フィールド値を変更するためのハンドラーのジョブ

フィールドの値を変更するときに、何らかのロジックを実行する必要があるとします。 もちろん、get / setメソッドでプロパティを定義し、目的のコードをセットに入れることができます。 そして、デコレータを宣言することでコードの量を減らすことができます:
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; } }
definePropertyを呼び出し、デコレータからハンドルを返すことに注意してください。 これは、フィールドデコレータの戻り値が無視される、 reflect-metadataのバグが原因です。
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;
実行結果:
» Plunkerで実際に試してみてください
» プレイグラウンドで見る
ハンドラーを静的として宣言する必要がありました。なぜなら インスタンスメソッドを参照することは困難です。 文字列パラメーターを使用する代替手段と 、ラムダを使用するもう1つです。
注入 - 依存性注入

デコレータの興味深い機能の1つは、装飾するプロパティまたはパラメータのタイプに関する情報を取得できることです(Angularに「ありがとう」と言うのは、それが特別に行われたためです)。 これが機能するには、reflect-metadataライブラリを接続し、emitDecoratorMetadataオプションを有効にする必要があります(上記を参照)。 その後、少なくとも1つのデコレータを持つプロパティの場合、キー「design:type」を使用してReflect.getMetadataを呼び出し、対応するタイプのコンストラクターを取得できます。 以下は、 @Inject
デコレータの単純な実装で、この手法を使用して依存関係を@Inject
ます。
// 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; }
definePropertyを呼び出し、デコレータからハンドルを返すことに注意してください。 これは、フィールドデコレータの戻り値が無視される、 reflect-metadataのバグが原因です。
// , , 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; } }
ご覧のとおり、logServiceフィールドを宣言するだけで、デコレーターはそのタイプを独自に決定し、サービスの対応するインスタンスを受け取るアクセス方法を設定します。 素敵で快適。 実行結果:
» Plunkerでお試しください
» プレイグラウンドで見る
@JsonName- 変換を伴うモデルのシリアル化

何らかの理由で、JSONでシリアル化するときにオブジェクトの一部のフィールドの名前を変更する必要があるとします。 デコレータを使用して、フィールドのJSON名を宣言し、シリアル化後にそれを読み取ることができます。 技術的には、このデコレーターは、reflect-metadataライブラリー、特にReflect.defineMetadataおよび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);
実行結果:
» Plunkerでお試しください
» プレイグラウンドで見る
指定されたデコレーターには、モデルにフィールドとして他のクラスのオブジェクトが含まれている場合、これらのクラスのフィールドがserializeメソッドによって処理されないという欠点があります(つまり、@ JsonNameデコレーターをそれらに適用できない)。 また、ここではJSONからクライアントモデルへの逆変換は実装されていません。 これらの欠点は両方とも、サーバーモデルコンバーターの実装がやや複雑で、以下のネタバレで修正されています。
@ServerModelField-デコレータのサーバーモデルコンバーター
問題のステートメントは次のとおりです。 このタイプのいくつかのJSONデータがサーバーから到着します(同様のJSONが1つのBaaSサービスを送信します)。
{ "username":"PFight77", "email":"test@gmail.com", "doc": { "info":"The author of the article" } }
このデータを型付きオブジェクトに変換し、いくつかのフィールドの名前を変更します。 最終的に、すべては次のようになります。
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); } }
これがどのように実装されているか見てみましょう。
最初に、文字列パラメーターを取得してメタデータに保存するServerModelField フィールドデコレーターを定義する必要があります。 さらに、JSONを解析するために、デコレータを持つフィールドが一般的にクラスにあることを知る必要があります。 これを行うには、クラスのすべてのフィールドに共通のメタデータの別のインスタンスを宣言します。このインスタンスには、装飾されたすべてのメンバーの名前が保存されます。 ここでは、Relect.defineMetadataを介してメタデータを保存するだけでなく、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); } }
さて、関数convertFromServerを書くことは残っています。 特別なことはほとんどありません。Reflect.getMetadataを呼び出し、受信したメタデータを使用してJSONを解析します。 1つの機能-この関数は、newを介してUserInfoのインスタンスを作成する必要があるため、JSONデータに加えてクラス: convertFromServer(JSON.parse(data), UserInfo)
ます。 これがどのように機能するかを理解するには、以下のネタバレをご覧ください。
class MyClass { } // " " var myType: { new(): any; }; // myType = MyClass; // new MyClass() var obj = new myType();
2番目の機能は、tsconfig.jsonで「emitDecoratorMetadata」:trueを設定することにより生成されるフィールドタイプデータの使用です。 レセプションは、キー「design:type」をReflect.getMetadata
してReflect.getMetadata
を呼び出すことで構成され、対応するタイプのコンストラクターを返します。 たとえば、 Reflect.getMetadata("design:type", target, "mAdditionalInfo")
をUserAdditionalInfo
と、 UserAdditionalInfo
コンストラクターが返されます。 カスタムタイプフィールドを適切に処理するために、この情報を使用します。 たとえば、UserAdditionalInfoクラスは@ServerModelFieldデコレーターも使用するため、このメタデータを使用してJSONを解析する必要もあります。
3番目の機能は、メタデータを取得する適切なターゲットを取得することです。 フィールドデコレータを使用しているため、クラスプロトタイプからメタデータを取得する必要があります。 静的メンバーデコレータの場合、クラスコンストラクターを使用する必要があります。 プロトタイプを取得するには、 Object.getPrototypeOfを呼び出すか、コンストラクターのプロトタイププロパティにアクセスします。
コード内の他のすべてのコメント:
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>"; } }
逆関数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 .
Controller , Action —

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"});
:
» Plunker
» Playground
, TypeScript . JSON. , , ( , Controller ).
おわりに
TypeScript , . - ++, .
, :
. , , , .. , .
Reflect. , , .
- Reflect.getMetada "design:type".
, . , 8 . ( ), . , API ReactJS , this .
今のところすべてです。 , .
UPD。 whileTrue , ES7 . , ES8 .
