LoginSignup
142

More than 5 years have passed since last update.

TypeScriptのDecoratorメモ

Last updated at Posted at 2015-04-07

※ このエントリは策定中の仕様や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のメソッド宣言部
  • メソッドの引数
@someDecorator
class MyClass {
  @someDecorator
  message: string;

  constructor(@someDecorator private name: string){}

  @someDecorator
  greeting(): void {}

  @someDecorator
  static sayHello(): void {}
}

ES7のproposal では, Objectリテラル中のmethod, property accessorにも付与できるとのことだが, TypeScript, Babel共に現状未対応の模様.

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

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

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

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の解説)

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

desugar

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

ts
@someClassDecorator
class MyClass {
  // ...
}
es5
var MyClass = (function {
  function MyClass() {
    // ...
  }
  return someClassDecorator(MyClass);
})()

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

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

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

引数付きのDecorator

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

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

引数付き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にて, @Component@Template というDecoratorが出てくるが, このパターンの典型例である.

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

JavaScript, TypeScriptならではのDecorator活用パターン?

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

Case Study

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

Case 1. AOP

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

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);
実行例
tsc -t es5 loggingSample.ts; node loggingSample.js
add in: 1,4
add out: 5

Case 2. xUnit的な何か

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

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 みたいだ(それが良いことなのかどうかはさておき).

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

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 が元ネタ.

AngularJSにDecoratorを導入する例だ.

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

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

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を使うと少しすっきりする.

/// <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を用意すると下記のようになる.

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とは逆だったので要注意な気配がプンプンする(サンプル).
今の段階では, 複数のDecoratorを許容するのなら, 交換可能であるように実装するしかないのかな。

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

まとめ

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

参考:

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
142