TypeScriptのDecoratorメモ

※ このエントリは策定中の仕様やfix前の実装に従って書いています(2015.04.07). 仕様の状況によっては, 記載内容に不整合が出る可能性があります.

## Decoratorの基本

TypeScriptの1.5.0-alphaから Decoratorが利用可能になった.

Decoratorは `@Component` のように記述して付加情報をclassやmethod等に付与するための仕組みで, ES7の仕様として検討されている(2015.04.07時点でstage1).

TypeScript 1.5.0-alphaでは, Decoratorを付与できる箇所は下記である.

* class 宣言部
* classのプロパティ宣言部
* classのメソッド宣言部
* メソッドの引数

```typescript
@someDecorator
class MyClass {
  @someDecorator
  message: string;

  constructor(@someDecorator private name: string){}

  @someDecorator
  greeting(): void {}

  @someDecorator
  static sayHello(): void {}
}
```

[ES7のproposal](https://github.com/wycats/javascript-decorators) では, Objectリテラル中のmethod, property accessorにも付与できるとのことだが, TypeScript, Babel共に現状未対応の模様.

逆に下記のような場所にはDecoratorは付与できない.

* module
* constructor
* 関数(`let someFunc = function(){...}`)

Decoratorは関数として実装する. Decoratorのsignatureは付与対象に応じて異なる.
先に列挙した付与対象に応じて, それぞれ, `ClassDecorator`, `PropertyDecorator`, `MethodDecorator`, `ParameterDecorator` となる.

```typescript:lib.core.d.tsから抜粋
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: Function, propertyKey: string | symbol, parameterIndex: number) => void;
```

MethodDecoratorの引数に出現する `TypedPropertyDescriptor` はES5から導入されたPropertyDescriptorに対応している([MDN: PropertyDescriptorの解説](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty))

`tsc` で`-t es5` を付けずにDecoratorを含んだコードをコンパイルすると怒られるのは, ES3にはPropertyDescriptorが存在しないからだ.

### desugar

さて, Decoratorはcompile後にes5コードにdesugarされる. 一番簡単なClassDecoratorを例にすると, 変換イメージは下記のようになる.

```typescript:ts
@someClassDecorator
class MyClass {
  // ...
}
```

```javascript:es5
var MyClass = (function {
  function MyClass() {
    // ...
  }
  return someClassDecorator(MyClass);
})()
```

Decoratorにはコンストラクタが引き渡されるため, prototypeを弄ってしまえば, 元々のclassには存在しないプロパティを追加することもできる.  "Decorator" と呼ばれる所以だ.

```typescript
let someClassDecorator = (clazz: Function) => {
  clazz.prototype.hogehoge = 'FOOFOO';
  return clazz;
};
```

上述のsignatureの通り, MethodDecoratorも PropertyDecorator型の戻り値を返すことができ、PropertyDecorator#valueの値を変更することで, 本来のメソッドの挙動を変更することができる(コーディング例は後述).


### 引数付きのDecorator

Decorator利用時に任意の引数を渡すこともできる.

引数付きのDecoratorを作成する場合, 先述したdecoratorのsignatureを持つfunctionを返却する関数を用意すればよい.

```typescript:引数付きClassDecoratorの例
let someClassDecorator = (/* Decoratorの引数 */ option?: string) => {
  return <TFunc extends Function> (target: TFunc) => {
    // ...
  };
};

@someClassDecorator('オプション')
class MyClass {}
```

## 何をDecoratorとして括りだすべきか.
さて, Decoratorはどのような場合に用いるべきだろうか？

先述の通り, Decoratorを使えば, classのprototypeやmethod本体に侵襲できるため, その気になれば何でもできる.
もともとのclass定義をガン無視して、全く関係ないコンストラクタを返すことだってできるが, そんなことしても誰の特にもならない.

僕は元々エンプラ系のJavaをやっていた時期があり, どうしてもJavaのAnnotationとDecoratorとの比較になってしまうが, JavaにおけるAnnotationの利用例を考えると, 頻出するパターンは下記と思う.

### AOP
Aspect(コンポーネントの主処理と直交する関心事)を処理させるためにDecoratorを作成するケース.
良く例に挙がるテーマはロギングや異常系ハンドリング(例外処理), トランザクション境界の宣言, 透過参照関数のcache, memoize等.

### フレームワークとの協調動作
例えば, DIコンテナへComponent情報を登録するようなパターン.
[AngularJS 2 quickstart](https://angular.io/docs/js/latest/quickstart.html)にて, `@Component` や `@Template` というDecoratorが出てくるが, このパターンの典型例である.

いずれにせよ、副次的な処理と主処理を分けて, 副次的処理をDecoratorの裏に隠蔽化することが目的である.
まぁどこまでが「副次的」なのかは、考え方次第な気もするが...
(例えば, JavaのJAX-Bなんて只のJava Beanに対して大量のAnnotationを付与するため, Annotationである必要性が全く分からなかった)

### JavaScript, TypeScriptならではのDecorator活用パターン?
僕の貧弱な想像力では Java とのアナロジしか思い浮かばなかったが, JavaScriptやTypeScriptならではなDecorator利用パターンはないのかしら...
何か面白い活用方法を知っている・思いついた等があれば, 教えてください.

## Case Study

ここからは, 実際のコード例で話を進めていく.

### Case 1. AOP

以下はメソッド実行時に引数と戻り値の情報をコンソールに吐き出すMethodDecoratorのサンプル.
ロギングを主処理から分離した例である.

<!-- {{{ -->

```typescript:loggingSample.ts
let debug = (target: any, name: string, descriptor: PropertyDescriptor) => {
  let delegate = descriptor.value;
  descriptor.value = function () {
    let args: string[] = [];
    for (let i = 0; i < arguments.length; i++){
      args.push(arguments[i]);
    }
    console.log(`${name} in: ${args.join()}`);
    let result = delegate.apply(this, arguments);
    console.log(`${name} out: ${result}`);
    return result;
  };
  return descriptor;
};

class Calc {
  @debug
  add (a: number, b: number): number{
    return a + b;
  }
}

new Calc().add(1, 4);
```

<!-- }}} -->

```text:実行例
tsc -t es5 loggingSample.ts; node loggingSample.js
add in: 1,4
add out: 5
```

### Case 2. xUnit的な何か

Decoratorを使ったテスティングFWの例である.
(MochaやChaiに習って、sayu(白湯)という名前にしてみた. 只の言葉遊び)

<!-- {{{ -->

```typescript:testFwSample.ts
// テスティングフレームワーク相当. 白湯.
module sayu {

  export function test (target: any, name: string, descriptor: PropertyDescriptor): void {
    let val = descriptor.value;
    val.__test = true;
  }

  export function suite (target: any): void {
    let testSuite = new target();
    let successCount = 0;
    let failCount = 0;
    for ( let key in target.prototype) {
      if(target.prototype[key].__test) {
        console.log(`Start test case ${key}`);
        try {
          testSuite[key]();
          successCount++;
          console.log(' OK')
        } catch (e) {
          failCount++;
          console.error(` Fail to assert. ${e.message}`);
        }
        
      }
    }
    console.log(`${successCount} cases succeed, ${failCount} cases fail`);
  }

  export module assert {
    export function assertEquals <T> (expected: T, actual: T) {
      if (expected !== actual) {
        throw new Error(`Assertion fail. Expected: ${expected}, but Actual: ${actual}.`)
      }
    }
  }
}

// 利用側
import suite = sayu.suite;
import test = sayu.test;
import assertEquals = sayu.assert.assertEquals;


let fizzBuzz = function (n: number): string {
  if(n % 15 === 0) {
    return 'FizzBuzz';
  }else if(n % 5 === 0) {
    return 'Buzz';
  }else if(n % 3 === 0) {
    return 'Fizz';
  }else {
    return n + '';
  }
}

@suite
class fizzbuzzTest {

  @test
  入力が1() {
    assertEquals('1', fizzBuzz(1));
  }

  @test
  入力が3() {
    assertEquals('Fizz', fizzBuzz(3));
  }

  @test
  入力が5() {
    // Fail to assert
    assertEquals('5', fizzBuzz(5));
  }

}
```
<!-- }}} -->

classに付与するDecoratorを作り, その内部でテストメソッドを呼び出すように実装している.

まるで [JUnit 4.x](http://junit.org/) みたいだ(それが良いことなのかどうかはさておき).

上記のコードを実行すると, ↓のようになる.

<!-- {{{ -->

```bash
tsc -t es5 testFwSample.ts; node testFwSample.js
```

```
Start test case 入力が1
 OK
Start test case 入力が3
 OK
Start test case 入力が5
 Fail to assert. Assertion fail. Expected: 5, but Actual: Buzz.
2 cases succeed, 1 cases fail
```

<!-- }}} -->

### Case 3. AngularJS
最後にもう少し実用的な例を一つ. @vvakame氏の[gist](https://gist.github.com/vvakame/29a0f26c9b64877354c6) が元ネタ.

AngularJSにDecoratorを導入する例だ.

AngularJSのControllerやServiceのコンポーネントは, コンストラクタ関数をモジュールに登録させるため, TypeScriptのclassと相性がよい.
また, Angular 2.xでは, さらにClassによるコンポーネント指向, Decoratorとの連携が進む見込みなので, 今のうちから慣れておく意味もこめて、Angular 1.xでもDecoratorを使ってみる.

まずはDecoratorを使わずに, TypeScriptでAngularJS 1.x のコードを書いてみる.

<!-- {{{ -->

```typescript:ngMyApp.ts
/// <reference path="../typings/angularjs/angular.d.ts" />

interface NgMyApp extends ng.IModule {}
let ngMyApp: NgMyApp = angular.module('ngMyApp', []);

class MainCtrl {
  message: string;
  constructor() {
    this.message = 'Hello';
  }
}

// モジュールへの登録
ngMyApp.controller('MainCtrl', [MainCtrl]);
```

<!-- }}} -->

htmlテンプレート上からは, `ng-controller="MainCtrl as main"` のようにcontrollerAs Syntaxを利用することで, `{{main.message}}` のようにMainCtrlのpropertyを参照できる.

このままでも十分使えるのだが, 少し鬱陶しいのが, コード末尾で登場する`ngMyApp.controller(...)` の部分.

この部分にDecoratorを使うと少しすっきりする.

<!-- {{{ -->

```typescript
/// <reference path="../typings/angularjs/angular.d.ts" />

interface ModuleOption {
  moduleName: string;
}

interface NamedFunction extends Function {
  name: string;
}

let controller = (opt: ModuleOption) => {
  return (controllerClazz: Function) => {
    let clazz = <NamedFunction>controllerClazz;
    angular.module(opt.moduleName).controller(opt.componentName || clazz.name, clazz);
  };
};

interface NgMyApp extends ng.IModule {}
let ngMyApp: NgMyApp = angular.module('ngMyApp', []);

@controller({moduleName: 'ngMyApp'})
class MainCtrl {
  message: string;
  constructor() {
    this.message = 'Hello';
  }
}
```

<!-- }}} -->

`@controller` Decoratorを用意し, Decorator内部にmodule登録処理を共通化する.

Decorator定義の分, 元々のコードと比較して量が増えてはいるものの, Controllerが大量になっても, `@controller` を付与すれば済むようになった.

このままだとminifyかけたときに`injector.annotate` が空振りする可能性があるので, `$inject` を明示するためのDecoratorも用意してみた.
また, Serviceも同様にDecoratorを用意すると下記のようになる.

<!-- {{{ -->

```typescript:ngMyApp_with_decorator.ts
/// <reference path="../typings/angularjs/angular.d.ts" />

// Decorators.
interface ModuleOption {
  moduleName: string;
  componentName?: string;
}

interface NamedFunction extends Function {
  name: string;
}

let inject = (injectableNames: string[]) => {
  return (target: Function) => {
    target.$inject = injectableNames;
  };
};

let controller = (opt: ModuleOption) => {
  return (controllerClazz: Function) => {
    let clazz = <NamedFunction>controllerClazz;
    angular.module(opt.moduleName).controller(opt.componentName || clazz.name, clazz);
  };
};

let service = (opt: ModuleOption) => {
  return (serviceClazz: Function) => {
    let clazz = <NamedFunction>serviceClazz;
    angular.module(opt.moduleName).service(opt.componentName || clazz.name, clazz);
  }
}

// Application.

interface NgMyApp extends ng.IModule {}
let ngMyApp: NgMyApp = angular.module('ngMyApp', []);

@service({moduleName: 'ngMyApp', componentName: 'greeter'})
class Greeter {
  sayHello(): string {
    return 'Hello, ng & ts!';
  }
}

@controller({moduleName: 'ngMyApp'})
@inject(['$window', 'greeter'])
class MainCtrl {
  message: string;
  constructor(private $window: ng.IWindowService, greeter: Greeter) {
    this.message = greeter.sayHello();
  }
}
```

<!-- }}} -->

Angular 1.x のアプリをTypeScriptで開発する機会があればお試しあれ.

## Decoratorの動作順序(2015.04.13 追記)
最後のAngularの例では, classに複数のDecoratorを張っていたが, TypeScriptでは下→上の順番で評価される. 先述の例で言えば, `inject()` → `controller()` の順番.
~~そういうもんか, くらいの気持ちでいたが, BabelのDecoratorsを試して見たところ、上→下の動作順序でTypeScriptとは逆だったので要注意な気配がプンプンする([サンプル](https://babeljs.io/repl/#?experimental=true&evaluate=true&loose=false&spec=false&code=let%20decoA%20%3D%20()%3D%3E%7B%7D%0Alet%20decoB%20%3D%20()%3D%3E%7B%7D%0A%0A%40decoA%0A%40decoB%0Aclass%20someClass%7B%7D)).
今の段階では, 複数のDecoratorを許容するのなら, 交換可能であるように実装するしかないのかな。~~

* さらに追記(2015.04.13)  
上記の愚痴をTwitterでこぼしてたら, [その日のうちにBabelに修正が入った](https://github.com/babel/babel/commit/bbc951dfe191c3be507816b92c69e259c28cbdb2). Babelも下→上の動作順序になりました.
@sebmac さんマジぱねっす(しかも日本語のtweet翻訳して追っかけてるとか凄すぎる).

## まとめ

* Decoratorの基本は, classやmethodをラップする関数.
* Decoratorにaspectを実装すればAOPっぽいことが実現できる.
* classやmethod定義の拡張ポイントとして利用することで, Decorate対象をフレームワークに連携可能.
* Decoratorの動作順序には気を使うべし

## 参考:

* [ES7 Decorators proposal](https://github.com/wycats/javascript-decorators)
* [PropertyDescriptorについてのMDN解説](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)
* [ES7 Decoratorに関するスライド](http://azu.github.io/slide/typescript-sushi/decorators.html)
* [AngularのModule登録についてのgist](https://gist.github.com/vvakame/29a0f26c9b64877354c6)