TypeScriptのDecoratorまとめ

# はじめに
Decorator使ってみることになったが何もわからないのでまとめる。

# TL;DR.
[コード](https://github.com/Tetsuya-Minase/program-samples/tree/master/decorator_sample)

# 準備
デフォルトだと使えないので、下記の通り`tsconfig`を修正する必要がある。
おそらく`tsc —init`の結果に`"experimentalDecorators": true`の追加で問題ないはず。
(cliの場合同様のオプションを指定すればOK)

```typescript
{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}
```

# 使い方
`@hoge`の形式で`class`, `method`, `accessor`, `property`, `parameter`につけられる。
ただし、`hoge`はdecoratorを付けた場所の情報と共に実行時呼び出される関数である必要がある。
＋型定義とか`declare class`にはつけられない。
何言っているか自分でもわからないので、使い方はサンプルを添えてまとめる。

## Class Decorators
Class Decoratorはクラスのコンストラクターに適用され、
クラス定義の監視、変更、置換のために使用できる。
使うときはclassの前に付ければ良い。

``` typescript
// classDecoratorは引数に付けたクラスのconstrouctorを受け取る
function classDecorator(constructor: Function) {

}

@classDecorator
class SampleClass {
}
```

使い方は下記の様にする。
下記の例では`constructor`とその`prototype`に`Object.seal`をかけている。

``` typescript
function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}
```

Class Decoratorが値を返却する時は、その値でconstructorの内容を上書きできる。

``` typescript
function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
  return class extends constructor {
      newProperty = "new property";
      hello = "override";
  }
}

@classDecorator
class Greeter {
  property = "property";
  hello: string;
  constructor(m: string) {
      this.hello = m;
  }
}

console.log(new Greeter("world"));
```

出力結果は下記の通り。
constructorの結果が上書きされていることがわかる。

```javascript
class_1 {
  property: 'property',
  hello: 'override',
  newProperty: 'new property'
}
```

## Method Decorators
method decoratorはメソッドに適用でき、
メソッド定義の観察、変更、または置換に使用できる。
使う時はmethodの前に付ければ良い。

``` typescript
    /**
     * 下記のpropertyを受け取る
     * @param target decoratorを付けたmethodのclassのprototype
     * @param propertyKey decoratorを付けたmethodの名前
     * @param descriptor methodのproperty descriptor ※targetがes5未満だとundefinedになる
     */
    function methodDecoratorSample(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  
    }
  
    class SampleClass {
      @methodDecoratorSample
      greet(message: string) {
        return `Hello! ${message}`;
      }
    }
```

使い方は下記の通り。
`descriptor.value`の結果を書き換えることで、メソッドの戻り値を変更できる。
`arguments`を使うことでdecorator付けたメソッドの引数を取れる。

``` typescript
function greetDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  descriptor.value = function () {
    return `こんにちは。${arguments[0]}`;
  };
}

class MethodDecoratorGreeter {
  @greetDecorator
  public greet(name: string) {
    return `Hi! ${name}`;
  }
}
console.log(new MethodDecoratorGreeter().greet('Tom')); // こんにちは。Tom
```

元のメソッドを実行したい場合は、`Reflect`を使うことでできる。
また、decoratorで引数を受け取りたい場合は、
必要な引数3つを受け取る関数を返す関数を作ることでできる。

``` typescript 
function reflectDecorator(type: 'original' | 'change') {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // 後で実行したいので退避しておく
    const originalMethod = descriptor.value;
    switch(type) {
      case 'original':
        descriptor.value = function() {
          return Reflect.apply(originalMethod, this, arguments);
        }
        break;
      case 'change':
        descriptor.value = function() {
          return '変更した';
        }
    }
  }
}

class ReflectSample {
  @reflectDecorator('original')
  noCange() {
    return '元のメソッド';
  }

  @reflectDecorator('change')
  change() {
    return '元のメソッド';
  }
}
const reflectSample = new ReflectSample();
console.log(`noChange: ${reflectSample.noCange()}`); // 元のメソッド
console.log(`change  : ${reflectSample.change()}`); // 変更した
```

## Accessor Decorators
method decoratorのaccessor版。
ほぼ同じなので割愛。

## Property Decorators
今までのDecoratorとは違い、あまりできることが多くないかも？
使う時はpropertyの前に付ければ良い。

下記のように`PropertyDescriptor`を指定することで、
propertyの内容を変更できる。
ただし、decoratorの戻り値は`void` or `any`なので実質型は消え去る。
(ここだけなのでこだわる必要もあまりないと思うけど)

``` typescript 
/**
 * 下記の引数を受け取る
  * @param target decoratorを付けたpropertyのclassのprototype
  * @param member プロパティ名
  */
function propertyDecoratorSample(target: any, member: string): any {
  const propertyDescriptor: PropertyDescriptor = {
    configurable: false,
    enumerable: false,
    value: 'huga',
    writable: true
  }
  return propertyDescriptor;
}


class SampleClassProperty {
  @propertyDecoratorSample
  private name?: string;

  constructor(name: string) {  
    // 上書きする前は、decoratorの戻り値で指定した内容になっている
    this.name = name;
  }
}

console.log(new SampleClassProperty('hoge'));// SampleClassProperty { name: 'hoge' }
```

## Parameter Decorators
ParameterDecoratorsは、メソッドでパラメータが宣言されたことを確認するためにのみ使用できる。
ParameterDecoratorsの戻り値は無視される。
使う時はparameterの前に付ければ良い。

``` typescript 
import 'reflect-metadata';

const metaDataKey = Symbol('sample');

/**
 * 下記のpropertyを受け取る
  * @param target decoratorを付けたclassのprototype
  * @param member memberの名前
  * @param parameterIndex 関数のパラメーターリスト内のパラメーターのインデックス
  */
function parameterDecoratorSample(target: any, member: string, parameterIndex: number) {
  const parameters = Reflect.getOwnMetadata(metaDataKey, target, member) || [];
  parameters.push(parameterIndex);
  // metadata付与
  Reflect.defineMetadata(metaDataKey, parameters, target, member);

}

function methodDecorator(target: any, propKey: string, desc: PropertyDescriptor) {
  const method = desc.value;
  desc.value = function() {
    // 付与したメタデータ取得
    const parameters = Reflect.getMetadata(metaDataKey, target, propKey);
  
    // パラメータチェック
    if (parameters) {
      for (const parameterIndex of parameters) {
        if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined ) {
          throw new Error('Missing required argument!');
        }
      }
    }
    // 問題なければ元のメソッドを実行する
    return method.apply(this, arguments);
  }

}

class SampleClassParameter {
  @methodDecorator
  greet(@parameterDecoratorSample name: string) {
    return `Hello! ${name}`;
  }
}

console.log(new SampleClassParameter().greet('Tom'));
```

# まとめ
TypeScriptのDecoratorsについてまとめた。
思ったより便利ではあったが、
一部`Reflect`を使わないと行けないところもあり、  複雑なところもある印象。
ただ、便利なので使える場所では使っていきたいところ。

ちなみに、DecoratorはECMAScriptでもstage2なので、いずれJSにも来るかもしれない。

それでは今回はこの辺で。

# 参考リンク
- [Decorators · TypeScript](https://www.typescriptlang.org/docs/handbook/decorators.html)