LoginSignup
25
26

More than 5 years have passed since last update.

TypeScript 1.5 Alphaに入ったDecoratorsは何をするのか

Last updated at Posted at 2015-04-08

皆さん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のサンプルとしてこんなコードを書いてみました。

decorators.ts
@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がこちら。

decorators.js
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に渡されたtargetGreeter.prototypeを指しているので、上記のような実行結果となる。

メソッド部分

Object.defineProperty(Greeter.prototype, "hello", __decorate([decorateMethod], Greeter.prototype, "hello", Object.getOwnPropertyDescriptor(Greeter.prototype, "hello")));
  1. Object.getOwnPropertyDescriptorで対象のプロパティディスクリプタを取得
  2. __decorateには引数を4つ渡しており、kindはプロパティディスクリプタを渡しているvalueを見てobjectと判定される
  3. objectに合わせた引数が渡され、Decoratorsが実行される
  4. __decorateでプロパティディスクリプタが返ってくるので、プロパティを再定義する

以上がメソッドについてのDecoratorsであり、その実行結果である。

クラス部分

Greeter = __decorate([decorateClass], Greeter);

kindの取得部分で、引数が2つであるためkindtargetであるGreeterとなる。
これはFunctionであるため対象のDecoratorsが実行される。
Decoratorsに渡されたvalueGreeterを指しているので、上記のような実行結果となる。

ざっくりまとめ

ここまでの結果からとりあえず分かること
1. プロパティよりあとにクラスのDecoratorsが評価される
2. 生成された__decoratedecoratorsをfor文でループさせている
3. 最終的に型が合えばdecoratorsとしての関数はコンパイルが通る
4. 現状Object.defineProperty周りの知識は必要

プロパティよりあとにクラスのDecoratorsが評価される

これは当たり前ですが、プロパティに対してのDecoratorsを評価したあとでなければ意味がないからですね。
例えばクラスに対してのDecoratorsが先に実行されてprototypeからプロパティが削除されたら、プロパティに対してのDecoratorsが動作しなくなります。

生成された__decoratedecoratorsを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の処理の代替とかもできそうで今後に期待ですね。

参考資料

25
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
26