デコレータ

はじめに

TypeScriptとES6のクラスの導入により、クラスやクラスメンバの注釈付けや修正をサポートするための追加機能が必要なシナリオとして存在するようになりました。

デコレータは、クラス宣言とメンバの注釈(アノテーション)とメタプログラミング構文の両方を追加する方法を提供します。 デコレータはJavaScriptのステージ1の提案であり、TypeScriptの実験的な機能として利用できます。

注意: デコレータは実験的な機能であり、将来のリリースで変更される可能性があります。

デコレータの実験的なサポートを有効にするには、 experimentalDecoratorsのコンパイルオプションをコマンドライン、またはtsconfig.jsonのいずれかで有効にする必要があります。

コマンドライン:
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

デコレータ

デコレータは、 クラスメソッドアクセサプロパティ、 またはパラメータに割り当てることが出来る特別な種類の宣言です。 デコレータは@expressionという形式を使用し、 expressionはデコレーションされた宣言に関する情報とともに、実行時に呼び出される関数を評価しなければなりません。

例えば、デコレータ@sealedを指定すると、次のようにsealed関数を記述することができます。

function sealed(target) {
    // do something with 'target' ...
}

注意: クラスデコレータによるデコレータの詳細な例は後ほど説明します。

デコレータファクトリー

デコレータが宣言にどのように適用されるのかをカスタマイズしたい場合は、デコレータ・ファクトリを記述してそれを行います。 デコレータファクトリーは、実行時にデコレータに呼び出される式を返す単純な関数です。

私たちは次のような方法でデコレータ・ファクトリを書くことができます。

function color(value: string) { // これはデコレータファクトリーです
    return function (target) {  // これはデコレータです
        // do something with 'target' and 'value'...
    }
}

注意: メソッドデコレータによるデコレータの詳細な例は後ほど説明します。

デコレータの構造

次の例のように、複数のデコレータを宣言に適用することができます。

  • 1行の場合:
    @f @g x
    
  • 複数行の場合:
    @f
    @g
    x
    

複数のデコレータが1つの宣言に適用される場合、その評価は数学の合成関数(function composition)と似ています。 このモデルでは、関数fgを合成すると得られた複合体(f ∘ g)(x)f(g(x))と等価です。

そのため、TypeScriptの1つの宣言で複数のデコレータを評価する場合は、以下の手順を実行します。

  • 各デコレータの式は上から下に向かって評価されます。
  • そして、その結果は下から上へ関数として呼び出されていきます。

デコレータ・ファクトリを使用する場合、次の例でこの評価順を確認することができます。

function f() {
    console.log("f(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("f(): called");
    }
}

function g() {
    console.log("g(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("g(): called");
    }
}

class C {
    @f()
    @g()
    method() {}
}

これはコンソールで次のように出力されます。

f(): evaluated
g(): evaluated
g(): called
f(): called

デコレータの評価

クラス内の様々な宣言に対して適用されたデコレータが、どのように適用されるのかは明確に定義されています。

  1. メソッド、アクセサ、またはプロパティデコレータのあとに続くパラメーターデコレータは、各インスタンスメンバに適用されます。
  2. メソッド、アクセサ、またはプロパティデコレータのあとに続くパラメーターデコレータは、各静的メンバに適用されます。
  3. パラメーターデコレータはコンスタクタに適用されます。
  4. クラスデコレータはクラスに適用されます。

クラスデコレータ

クラスデコレータはクラス宣言の直前に宣言されます。 クラスデコレータは、クラスのコンストラクタに適用され、クラス定義の観察、変更、置換に使用できます。 クラスデコレータは、宣言ファイルやその他の環境コンテキスト(declareクラスなど)で使用することはできません。

クラスデコレータの式は、実行時に関数として呼び出され、 修飾されたクラスのコンストラクタが唯一の引数となります。

クラスデコレータが値を返す場合は、クラス宣言を指定されたコンストラクタ関数に置き換えます。

注意: 新しいコンストラクタ関数を返すようにする場合は、元のプロトタイプを維持するように注意する必要があります。 実行時にデコレータを適用するロジックは、これを行いません。

下記は、クラスデコレータ(@sealed)をGreeterクラスに適用している例になります。

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

@sealedデコレータは、次の関数宣言を使用して定義することができます。

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

@sealedが実行されると、コンストラクタとそのプロトタイプの両方が密封(seal())されます。

メソッドデコレータ

メソッドデコレータは、メソッド宣言の直前に宣言されます。 デコレータは、メソッドのプロパティ記述子(PropertyDescriptor)に適用され、メソッド定義を観察、変更、または置換するために使用できます。

メソッドデコレータは、宣言ファイル、オーバーロード、またはその他の環境コンテキスト(declareクラスなど)では、使用することはできません。

メソッドデコレータの式は、実行時に次の3つの引数が指定されて、関数として呼び出されます。

  1. 静的メンバのクラスのコンストラクタ関数、またはインスタンスメンバのクラスのプロトタイプのいずれかが指定されます。
  2. メンバの名前が指定されます。
  3. メンバのプロパティ記述子(PropertyDescriptor)が指定されます。

注意: スクリプトの対象がES5未満の場合、プロパティ記述子(PropertyDescriptor)はundefinedになります。

メソッドデコレータが値を返す場合は、メソッドのプロパティ記述子(PropertyDescriptor)として使用されます。

注意: スクリプトの対象がES5未満の場合、戻り値は無視されます。

下記の例では、Greeterクラスのメソッドにメソッドデコレータ(@enumerable)を適用しています。

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting;
    }
}

次の関数宣言を使用して@enumerableデコレータを定義できます。

function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.enumerable = value;
    };
}

@enumerable(false)デコレータは、ここではデコレータファクトリーです。 @enumerable(false)デコレータが呼び出されると、プロパティ記述子のenumerableプロパティを変更します。

アクセサデコレータ

アクセサデコレータは、アクセサ宣言の直前に宣言されます。 アクセサーデコレータは、アクセサのプロパティ記述子に適用され、アクセサの定義を観察、変更、または置換するために使用できます。

アクセサーデコレータは、宣言ファイルや他の環境コンテキスト(declareクラスなど)で使用することはできません。

注意: TypeScriptでは、単一のメンバに対してgetsetの両方のアクセサを装飾することはできません。 代わりに、メンバのすべてのデコレータをドキュメント順で最初に指定されたアクセサに適用させる必要があります。 これは、getおよびsetアクセサの宣言が別々にではなく、両方が結合されて、 プロパティ記述子(PropertyDescriptor)に適用されるためです。

アクセサデコレータの式は、実行時に次の3つの引数を指定して関数として呼び出されます。

  1. 静的メンバのクラスのコンストラクタ関数、またはインスタンスメンバのクラスのプロトタイプのいずれかが指定されます。
  2. メンバの名前が指定されます。
  3. メンバのプロパティ記述子(PropertyDescriptor)が指定されます。

注意: スクリプトの対象がES5未満の場合、プロパティ記述子(PropertyDescriptor)はundefinedになります。

アクセサデコレータが値を返す場合は、メソッドのプロパティ記述子(PropertyDescriptor)として使用されます。

注意: スクリプトの対象がES5未満の場合、戻り値は無視されます。

以下は、Pointクラスのメンバに適用されたアクセサーデコレータ(@configurable)の例です。

class Point {
    private _x: number;
    private _y: number;
    constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
    }

    @configurable(false)
    get x() { return this._x; }

    @configurable(false)
    get y() { return this._y; }
}

@configurableデコレータは、次の関数宣言を使用して定義できます。

function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}

プロパティデコレータ

プロパティデコレータは、プロパティ宣言の直前に宣言されます。 プロパティデコレータは、宣言ファイルや他の環境コンテキスト(declareクラスなど)で使用することはできません。

プロパティデコレータの式は、実行時に次の2つの引数を持つ関数として呼び出されます。

  1. 静的メンバのクラスのコンストラクタ関数、またはインスタンスメンバのクラスのプロトタイプのいずれかが指定されます。
  2. メンバの名前が指定されます。

注意: プロパティ指定子(Property Descriptor)はTypeScriptでプロパティデコレータがどのように初期化されたのかによって、 引数として提供

これは現在、プロトタイプのメンバを定義するときにインスタンスプロパティを記述する仕組みがなく、 プロパティの初期化子を観察または変更する方法がないためです。 そのため、プロパティデコレータは特定の名前のプロパティがクラスに対して宣言されたことを観察するためにのみ使用できます。

プロパティデコレータが値を返す場合は、そのメンバのプロパティ記述子として使用されます。

注意: スクリプトのターゲットがES5未満の場合、戻り値は無視されます。

次の例のように、この情報を使用してプロパティに関するメタデータを記録することができます。

class Greeter {
    @format("Hello, %s")
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        let formatString = getFormat(this, "greeting");
        return formatString.replace("%s", this.greeting);
    }
}

次の関数宣言を使用して、@formatデコレータ関数とgetFormat関数を定義することができます。

import "reflect-metadata";

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
    return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

ここでの@format("Hello, %s")デコレータは、デコレータファクトリーです。 @format("Hello, %s")が呼び出されると、 reflect-metadataライブラリのReflect.metadata関数を使用して、プロパティのメタデータエントリが追加されます。 getFormatが呼び出されると、そのフォーマットのメタデータ値が読み込まれます。

注意: この例ではreflect-metadataが必要になります。 reflect-metadataの詳細については、Metadataを参照してください。

パラメーターデコレータ

パラメーターデコレータは、パラメーター宣言の直前に宣言されます。 パラメーターデコレータはクラスのコンストラクタまたはメソッド宣言の関数に適用されます。 パラメータデコレータは、宣言ファイル、オーバーロード、 またはその他の環境コンテキスト(declareクラスなど)で使用することはできません。

パラメータデコレータの式は、実行時に次の3つの引数を指定して関数として呼び出されます。

  1. 静的メンバのクラスのコンストラクタ関数、またはインスタンスメンバのクラスのプロトタイプです。
  2. メンバの名前です。
  3. 関数のパラメーターリスト内のインデックスです。

注意: パラメーターデコレータは、メソッド上でパラメータが宣言されていることを観察するためにのみ使用できます。

パラメータデコレータの戻り値は無視されます。

以下は、Greeterクラスのメンバのパラメータに適用されるパラメータデコレータ(@required)の例です。

class Greeter {
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }

    @validate
    greet(@required name: string) {
        return "Hello " + name + ", " + this.greeting;
    }
}

次の関数宣言を使用して、@required@validateのデコレータを定義することができます。

import "reflect-metadata";

const requiredMetadataKey = Symbol("required");

function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
    let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
    existingRequiredParameters.push(parameterIndex);
    Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}

function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
    let method = descriptor.value;
    descriptor.value = function () {
        let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
        if (requiredParameters) {
            for (let parameterIndex of requiredParameters) {
                if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
                    throw new Error("Missing required argument.");
                }
            }
        }

        return method.apply(this, arguments);
    }
}

@requiredデコレータは、パラメーターが必要であると印付けるメタデータエントリを追加します。 @validateデコレータは、元のメソッドを呼び出す前に、引数を検証する関数を既存のgreetメソッドにラップします。

注意: この例ではreflect-metadataが必要になります。 reflect-metadataの詳細については、Metadataを参照してください。

Metadata

いくつかの例では試験的なメタデータAPIを使用するために、 そのpolyfillを追加するreflect-metadataライブラリを使用しています。 このライブラリは、まだECMAScript(JavaScript)標準の一部ではありません。 ただし、デコレータがECMAScript標準の一部として公式に採用されることになれば、これらの拡張の採用が提案されることになるでしょう。

このライブラリをnpmを通じてインストールすることができます。

npm i reflect-metadata --save

TypeScriptには、デコレータを持つ宣言に対して特定のタイプのメタデータを出力する実験的なサポートが含まれています。 この実験的なサポートを有効にするには、emitDecoratorMetadataコンパイラオプションをコマンドラインまたはtsconfig.jsonのいずれかで設定する必要があります。

コマンドライン
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
tsconfig.json
{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

有効にすると、reflect-metadataライブラリがインポートされれば、 追加のデザイン時(design-time)型情報が実行時に公開されます。

下記の例で、この動作を確認することができます。

import "reflect-metadata";

class Point {
    x: number;
    y: number;
}

class Line {
    private _p0: Point;
    private _p1: Point;

    @validate
    set p0(value: Point) { this._p0 = value; }
    get p0() { return this._p0; }

    @validate
    set p1(value: Point) { this._p1 = value; }
    get p1() { return this._p1; }
}

function validate<T>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {
    let set = descriptor.set;
    descriptor.set = function (value: T) {
        let type = Reflect.getMetadata("design:type", target, propertyKey);
        if (!(value instanceof type)) {
            throw new TypeError("Invalid type.");
        }
        set(value);
    }
}

TypeScriptコンパイラは、@Reflect.metadataデコレータを使用してデザイン時(design-time)型情報を注入します。 あなたはこれを下記のTypeScriptに相当すると考えることができます。

class Line {
    private _p0: Point;
    private _p1: Point;

    @validate
    @Reflect.metadata("design:type", Point)
    set p0(value: Point) { this._p0 = value; }
    get p0() { return this._p0; }

    @validate
    @Reflect.metadata("design:type", Point)
    set p1(value: Point) { this._p1 = value; }
    get p1() { return this._p1; }
}

注意: デコレータのメタデータは実験的な機能であり、将来のリリースで破壊的な変更が導入される可能性があります。

 Back to top

© https://github.com/Microsoft/TypeScript-Handbook

このページは、ページトップのリンク先のTypeScript-Handbook内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。

  • ドキュメントの情報が古い可能性があります。
  • "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。