水無瀬のプログラミング日記

プログラムの勉強した時のメモ

TypeScriptのDecoratorまとめ

はじめに

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

TL;DR.

コード

準備

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

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

使い方

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

Class Decorators

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

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

}

@classDecorator
class SampleClass {
}

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

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の内容を上書きできる。

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の結果が上書きされていることがわかる。

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

Method Decorators

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

    /**
     * 下記の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付けたメソッドの引数を取れる。

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つを受け取る関数を返す関数を作ることでできる。

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なので実質型は消え去る。
(ここだけなのでこだわる必要もあまりないと思うけど)

/**
 * 下記の引数を受け取る
  * @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の前に付ければ良い。

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にも来るかもしれない。

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

参考リンク