9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TypeScript Handbook を読む (19. Decorators)

Last updated at Posted at 2017-10-03

TypeScript Handbook を読み進めていく第十九回目。

  1. Basic Types
  2. Variable Declarations
  3. Interfaces
  4. Classes
  5. Functions
  6. Generics
  7. Enums
  8. Type Inference
  9. Type Compatibility
  10. Advanced Types
  11. Symbols
  12. Iterators and Generators
  13. Modules
  14. Namwspaces
  15. Namespaces and Modules
  16. Module Resolution
  17. Declaration Merging
  18. JSX
  19. Decorators (今ココ)
  20. Mixins
  21. Triple-Slash Directives
  22. Type Checking JavaScript Files

Decorators

原文

Introduction

TypeScript や ES6 でクラスを使用するにあたって、クラスやクラスメンバにアノテーションを付与することで振る舞いを変更するための機能が必要になる場合もあることでしょう。
デコレータはそのためのメタプログラミング構文を提供します。
デコレータは JavaScript の stage 2 proposal であり、TypeScript は試験的機能として提供されています。

デコレータを有効にするためにはコマンドラインまたは tsconfig.jsonexperimentalDecorators を有効にする必要があります。

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

Decorators

デコレータクラス宣言メソッドアクセサプロパティ引数 に付与することができる特殊な宣言です。
デコレータは @expression という形式で使用します。
この時、expression はデコレータが付与されている宣言の情報とともに実行時に関数として呼び出されます。

例えば、@sealed というデコレータに対し、以下のような sealed 関数を用意します。

TypeScript
function sealed(target) {
    // 'target' を使用して何かする...
}

Decorator Factories

宣言に対するデコレータの適用方法をカスタマイズしたい場合、デコレータファクトリを作成することができます。
デコレータファクトリ は実行時にデコレータから呼び出される式を返すだけの単純な関数です。

デコレータファクトリは以下のように作成します。

TypeScript
function color(value: string) { // デコレータファクトリ
    return function (target) { // デコレータ
        // 'target' と 'value' を使用して何かする...
    }
}

Decorator Composition

以下のように同一の宣言に対して複数のデコレータを適用することができます。

  • 単一行の例:
TypeScript
@f @g x
  • 複数行の例:
TypeScript
@f
@g
x

同一の宣言に対して複数のデコレータが適用されている場合、次の順で評価が行われます。

  1. 各デコレータの式が上から下へ評価される
  2. 上記の評価結果が関数として下から上へ評価される

デコレータファクトリ を使用すると、この評価順がよく分かります。

TypeScript
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

Decorator Evaluation

クラス内の各宣言に適用されたデコレータは以下の順で評価されます。

  1. インスタンスメンバに対する 引数デコレータメソッドデコレータアクセサデコレータプロパティデコレータ
  2. 静的メンバに対する 引数デコレータメソッドデコレータアクセサデコレータプロパティデコレータ
  3. コンストラクタに対する 引数デコレータ
  4. クラスに対する クラスデコレータ

Class Decorators

クラスデコレータ とはクラス宣言の直前に宣言するデコレータであり、コンストラクタに適用されることでクラス定義を監視、変更、置換することができます。
クラスデコレータは宣言ファイル、つまり (declare クラスといった) アンビエントコンテキスト内で使用することはできません。

クラスデコレータは実行時に関数として評価されますが、その引数として修飾されているコンストラクタを受け取ります。

もしクラスデコレータで新しいコンストラクタ関数を返却する場合、元の prototype を維持するように注意する必要があります。
なぜなら、デコレータに対してこの処理は 行われない ためです。

Greeter クラスに対するクラスデコレータ (@sealed) の適用例は以下の通りです。

TypeScript
@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
}());
Greeter = __decorate([
    sealed
], Greeter);

デコレータ経由でインスタンス化されるように、元のクラス宣言をラッピングするわけだ。

そして @sealed を以下のように定義したとします。

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

これにより、@sealed が実行されるとコンストラクタと prototype が変更不可になります。

次にコンストラクタをオーバーライドする例を見てみましょう。

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")); // Object { property: "property", hello: "override", newProperty: "new property" }
JavaScript
var __extends = (this && this.__extends) || (function () {
    var extendStatics = Object.setPrototypeOf ||
        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};

function classDecorator(constructor) {
    return (function (_super) {
        __extends(class_1, _super);
        function class_1() {
            var _this = _super !== null && _super.apply(this, arguments) || this;
            _this.newProperty = "new property";
            _this.hello = "override";
            return _this;
        }
        return class_1;
    }(constructor));
}

var Greeter = (function () {
    function Greeter(m) {
        this.property = "property";
        this.hello = m;
    }
    return Greeter;
}());
Greeter = __decorate([
    classDecorator
], Greeter);

console.log(new Greeter("world")); // Object { property: "property", hello: "override", newProperty: "new property" }

Greeter のコンストラクタが呼ばれた後に hello = "override" が実行されるため、結果として hello プロパティは "override" になるわけだ。

Method Decorators

メソッドデコレータ とはメソッド宣言の直前に宣言するデコレータであり、プロパティディスクリプタ に適用されることでメソッド定義を監視、変更、置換することができます。
メソッドデコレータはオーバーロードに対してや、宣言ファイル、つまり (declare クラスといった) アンビエントコンテキスト内で使用することはできません。

メソッドデコレータは実行時に関数として評価されますが、その引数として次の 3 つの引数を受け取ります。

  1. コンストラクタ関数 (静的メンバの場合) またはクラスの prototype (インスタンスメンバの場合)
  2. メンバの名前
  3. メンバの プロパティディスクリプタ
    (ただし、ES5 未満をターゲットとしている場合、プロパティディスクリプタundefined になります)

メソッドデコレータが値を返却した場合、メソッドに対するプロパティディスクリプタとして使用されます。
ただし、ES5 未満をターゲットとしている場合、返却値は無視されます。

Greeter クラスのメソッドにメソッドデコレータ (@enumerable) を適用する例は以下の通りです。

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

    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting;
    }
}
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
}());
__decorate([
    enumerable(false)
], Greeter.prototype, "greet", null);

そして @enumerable を以下のように定義したとします。

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

@enumerable(false)デコレータファクトリ であり、@enumerable(false) が実行されるとプロパティディスクリプタの enumerable プロパティが変更されます。

Accessor Decorators

アクセサデコレータ とはアクセサ宣言の直前に宣言するデコレータであり、プロパティディスクリプタ に適用されることでアクセサの定義を監視、変更、置換することができます。
アクセサデコレータは宣言ファイル、つまり (declare クラスといった) アンビエントコンテキスト内で使用することはできません。

注: TypeScript では単一のメンバに対して get アクセサと set アクセサの両方を修飾することを許可していません。
その代わりに、メンバの全デコレータはソースの記載順で最初のアクセサに適用されます。
これが個々のアクセサではなく、プロパティディスクリプタ (get アクセサと set アクセサの組) に対してデコレータが適用される理由です。

アクセサデコレータは実行時に関数として評価されますが、その引数として次の 3 つの引数を受け取ります。

  1. コンストラクタ関数 (静的メンバの場合) またはクラスの prototype (インスタンスメンバの場合)
  2. メンバの名前
  3. メンバの プロパティディスクリプタ
    (ただし、ES5 未満をターゲットとしている場合、プロパティディスクリプタundefined になります)

アクセサデコレータが値を返却した場合、メンバに対する プロパティディスクリプタ として使用されます。
ただし、ES5 未満をターゲットとしている場合、返却値は無視されます。

Greeter クラスのメソッドにメソッドデコレータ (@enumerable) を適用する例は以下の通りです。

TypeScript
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; }
}
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var Point = (function () {
    function Point(x, y) {
        this._x = x;
        this._y = y;
    }
    Object.defineProperty(Point.prototype, "x", {
        get: function () { return this._x; },
        enumerable: true,
        configurable: true
    });
    Object.defineProperty(Point.prototype, "y", {
        get: function () { return this._y; },
        enumerable: true,
        configurable: true
    });
    return Point;
}());
__decorate([
    configurable(false)
], Point.prototype, "x", null);
__decorate([
    configurable(false)
], Point.prototype, "y", null);

@configurable は以下のように定義することができます。

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

Property Decorators

プロパティデコレータ とはプロパティ宣言の直前に宣言するデコレータです。
プロパティデコレータは宣言ファイル、つまり (declare クラスといった) アンビエントコンテキスト内で使用することはできません。

プロパティデコレータは実行時に関数として評価されますが、その引数として次の 2 つの引数を受け取ります。

  1. コンストラクタ関数 (静的メンバの場合) またはクラスの prototype (インスタンスメンバの場合)
  2. メンバの名前

プロパティディスクリプタ は引数として渡されない点に注意してください
これは、現時点では prototype のメンバを定義する際にインスタンスプロパティを宣言するための方法がなく、プロパティの監視や変更を行うことができないためです。
また、プロパティデコレータの戻り値も無視されます。
そのため、プロパティデコレータは指定された名前のプロパティがクラス内に宣言されていることを監視するためにしか使用することはできません。

以下のように、引数で渡された情報を使用してプロパティに関するメタデータを記録しておくことが可能です。

TypeScript
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);
    }
}
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        var formatString = getFormat(this, "greeting");
        return formatString.replace("%s", this.greeting);
    };
    return Greeter;
}());
__decorate([
    format("Hello, %s")
], Greeter.prototype, "greeting", void 0);

@format デコレータと getFormat 関数は以下のように定義することができます。

TypeScript
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 が呼ばれた時にそのメタデータを読み出します。

Parameter Decorators

パラメータデコレータ とは引数宣言の直前に宣言するデコレータであり、クラスコンストラクタやメソッドに適用されます。
パラメータデコレータは宣言ファイル、つまり (declare クラスといった) アンビエントコンテキスト内で使用することはできません。

パラメータデコレータは実行時に関数として評価されますが、その引数として次の 3 つの引数を受け取ります。

  1. コンストラクタ関数 (静的メンバの場合) またはクラスの prototype (インスタンスメンバの場合)
  2. メンバの名前
  3. 関数の引数リスト内での序数

パラメータデコレータはメソッドにパラメータが宣言されていることを監視するためだけに使用できる点に注意してください。

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

Greeter クラスのメソッドの引数にパラメータデコレータ (@required) を適用する例は以下の通りです。

TypeScript
class Greeter {
    greeting: string;

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

    @validate
    greet(@required name: string) {
        return "Hello " + name + ", " + this.greeting;
    }
}
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function (name) {
        return "Hello " + name + ", " + this.greeting;
    };
    return Greeter;
}());
__decorate([
    validate,
    __param(0, required)
], Greeter.prototype, "greet", null);

@required デコレータと @validate デコレータは以下のように定義することができます。

TypeScript
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 メソッドをラップし、元の関数を呼び出す前にバリデーションを行います。

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 ライブラリをインポートすると、追加のデザイン時型情報が実行時にも参照できるようになります。

具体例は以下の通りです。

TypeScript
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 デコレータを使用してデザイン時型情報を埋め込みます。
つまり、以下の TypeScript コードと等価と言えます。

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; }
}

デコレータメタデータは試験的な機能であり、将来のリリースでは快適な変更が行われる可能性がある点に注意してください。

9
8
0

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
9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?