皆さんTypeScript使ってますか?
先にあげた ES6 modulesの記事に続いてDecoratorsについて書こうと思ってたらTypeScriptのDecoratorメモと先を越されてしまいました。
とはいえ自分なりに試してみた知見について書いておきます。
2015.04.09現在の内容です
Decoratorsとは
現在ES7の仕様として検討されている仕組みです。
クラスやメソッド、オブジェクトリテラル内のメソッドなど名前の前に@hogehoge
と書いておくことで、別途定義したhogehoge
という関数が呼び出され、ざっくり言えば対象を変更することができる。
TypeScriptではコンパイル時に--target ES5
もしくは--target ES6
を要求します。
その理由はこの後例で見せますがObject.defineProperty
を利用しているためで、このObject.defineProperty
についてはObject.defineProper | MDNもしくは@vvakameさんの「TypeScriptリファレンス 」にも記述があるので、参考までに。
サンプルコード
Decoratorsのサンプルとしてこんなコードを書いてみました。
@decorateClass
class Greeter {
@decorateProperty
message: string;
@decorateMethod
hello(name:string = "JavaScript") {
return `Hello, ${name}`;
}
}
function decorateClass(target) {
console.log(target);
return target
}
function decorateMethod(target, name, descriptor) {
console.log(`name: ${name}`);
console.log(target);
console.log(descriptor);
return descriptor;
}
function decorateProperty(target, name) {
console.log(`name: ${name}`);
console.log(target);
return target;
}
let greeter = new Greeter();
console.log(greeter.hello("TypeScript"));
そして変換されたあとのJavaScriptがこちら。
var __decorate = this.__decorate || function (decorators, target, key, value) {
var kind = typeof (arguments.length == 2 ? value = target : value);
for (var i = decorators.length - 1; i >= 0; --i) {
var decorator = decorators[i];
switch (kind) {
case "function": value = decorator(value) || value; break;
case "number": decorator(target, key, value); break;
case "undefined": decorator(target, key); break;
case "object": value = decorator(target, key, value) || value; break;
}
}
return value;
};
var Greeter = (function () {
function Greeter() {
}
Greeter.prototype.hello = function (name) {
if (name === void 0) { name = "JavaScript"; }
return "Hello, " + name;
};
__decorate([decorateProperty], Greeter.prototype, "message");
Object.defineProperty(Greeter.prototype, "hello", __decorate([decorateMethod], Greeter.prototype, "hello", Object.getOwnPropertyDescriptor(Greeter.prototype, "hello")));
Greeter = __decorate([decorateClass], Greeter);
return Greeter;
})();
function decorateClass(target) {
console.log(target);
return target;
}
function decorateMethod(target, name, descriptor) {
console.log("name: " + name);
console.log(target);
console.log(descriptor);
return descriptor;
}
function decorateProperty(target, name) {
console.log("name: " + name);
console.log(target);
return target;
}
var greeter = new Greeter();
console.log(greeter.hello("TypeScript"));
そしてnodeでの実行結果がこちら
$ node decorators.js
name: message
{ hello: [Function] }
name: hello
{ hello: [Function] }
{ value: [Function],
writable: true,
enumerable: true,
configurable: true }
[Function: Greeter]
Hello, TypeScript
Decoratorsの実行部分で何をしているのか
プロパティ部分
__decorate([decorateProperty], Greeter.prototype, "message");
kind
ではvalue
を参照するが、第4引数が渡されていないためundefined
となり、対象のDecoratorsが実行される。
Decoratorsに渡されたtarget
はGreeter.prototype
を指しているので、上記のような実行結果となる。
メソッド部分
Object.defineProperty(Greeter.prototype, "hello", __decorate([decorateMethod], Greeter.prototype, "hello", Object.getOwnPropertyDescriptor(Greeter.prototype, "hello")));
-
Object.getOwnPropertyDescriptor
で対象のプロパティディスクリプタを取得 -
__decorate
には引数を4つ渡しており、kind
はプロパティディスクリプタを渡しているvalue
を見てobject
と判定される -
object
に合わせた引数が渡され、Decoratorsが実行される -
__decorate
でプロパティディスクリプタが返ってくるので、プロパティを再定義する
以上がメソッドについてのDecoratorsであり、その実行結果である。
クラス部分
Greeter = __decorate([decorateClass], Greeter);
kind
の取得部分で、引数が2つであるためkind
はtarget
であるGreeter
となる。
これはFunction
であるため対象のDecoratorsが実行される。
Decoratorsに渡されたvalue
はGreeter
を指しているので、上記のような実行結果となる。
ざっくりまとめ
ここまでの結果からとりあえず分かること
- プロパティよりあとにクラスのDecoratorsが評価される
- 生成された
__decorate
がdecorators
をfor文でループさせている - 最終的に型が合えば
decorators
としての関数はコンパイルが通る - 現状
Object.defineProperty
周りの知識は必要
プロパティよりあとにクラスのDecoratorsが評価される
これは当たり前ですが、プロパティに対してのDecoratorsを評価したあとでなければ意味がないからですね。
例えばクラスに対してのDecoratorsが先に実行されてprototypeからプロパティが削除されたら、プロパティに対してのDecoratorsが動作しなくなります。
生成された__decorate
がdecorators
をfor文でループさせている
これは1つのクラス、プロパティに対して複数のDecoratorsが渡される可能性を考慮しているということです。
試しに以下のようにコードを変更してみます(一部抜粋)
@decorateClass
@decorateClass
class Greeter {
@decorateMethod
hello(name:string = "JavaScript") {
return `Hello, ${name}`;
}
}
var Greeter = (function () {
function Greeter() {
}
Greeter.prototype.hello = function (name) {
if (name === void 0) { name = "JavaScript"; }
return "Hello, " + name;
};
Object.defineProperty(Greeter.prototype, "hello", __decorate([decorateMethod], Greeter.prototype, "hello", Object.getOwnPropertyDescriptor(Greeter.prototype, "hello")));
Greeter = __decorate([decorateClass, decorateClass], Greeter);
return Greeter;
})();
クラスに対しての__decorate
の呼び出し部分で、第一引数であるdecorators
の配列の要素が増えていることがわかります。
ちょっと夢が広がる感じがしませんか?
何をするのがいいのか見つけられていませんが。
最終的に型が合えばdecorators
としての関数はコンパイルが通る
TypeScriptはlib.d.tsにあるDecoratorの型定義に合わなければコンパイルエラーになるので心配はあまりないですが、Babelとかのトランスパイラーは変なものが返ってきてないかチェックは大丈夫なんでしょうか。。。
ES7 Proposalということは未来ではこのObject.defineProperty
している部分等はブラウザが解釈することになり、Decoratorsとしての関数を定義するだけになるはずですが、返り値間違いの実行時エラーは怖いですね。
現状Object.defineProperty
周りの知識は必要
これは個人的に思っただけですが、@vvakameさんのAngularとDecoratorsの例のような単純なDecoratorsなら難しくはないと思われます。
しかしDecoratorsで上手にプロパティの制御を行いたいといった場合には覚えておいて損はないと思いました。
他にもKnockoutJSのko.observable
とか、Object.observe
の処理の代替とかもできそうで今後に期待ですね。