TypeScript Handbook を読み進めていく第十九回目。
- Basic Types
- Variable Declarations
- Interfaces
- Classes
- Functions
- Generics
- Enums
- Type Inference
- Type Compatibility
- Advanced Types
- Symbols
- Iterators and Generators
- Modules
- Namwspaces
- Namespaces and Modules
- Module Resolution
- Declaration Merging
- JSX
- Decorators (今ココ)
- Mixins
- Triple-Slash Directives
- Type Checking JavaScript Files
Decorators
Introduction
TypeScript や ES6 でクラスを使用するにあたって、クラスやクラスメンバにアノテーションを付与することで振る舞いを変更するための機能が必要になる場合もあることでしょう。
デコレータはそのためのメタプログラミング構文を提供します。
デコレータは JavaScript の stage 2 proposal であり、TypeScript は試験的機能として提供されています。
デコレータを有効にするためにはコマンドラインまたは tsconfig.json
で experimentalDecorators
を有効にする必要があります。
tsc --target ES5 --experimentalDecorators
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
Decorators
デコレータ は クラス宣言、メソッド、アクセサ、プロパティ、引数 に付与することができる特殊な宣言です。
デコレータは @expression
という形式で使用します。
この時、expression
はデコレータが付与されている宣言の情報とともに実行時に関数として呼び出されます。
例えば、@sealed
というデコレータに対し、以下のような sealed
関数を用意します。
function sealed(target) {
// 'target' を使用して何かする...
}
Decorator Factories
宣言に対するデコレータの適用方法をカスタマイズしたい場合、デコレータファクトリを作成することができます。
デコレータファクトリ は実行時にデコレータから呼び出される式を返すだけの単純な関数です。
デコレータファクトリは以下のように作成します。
function color(value: string) { // デコレータファクトリ
return function (target) { // デコレータ
// 'target' と 'value' を使用して何かする...
}
}
Decorator Composition
以下のように同一の宣言に対して複数のデコレータを適用することができます。
- 単一行の例:
@f @g x
- 複数行の例:
@f
@g
x
同一の宣言に対して複数のデコレータが適用されている場合、次の順で評価が行われます。
- 各デコレータの式が上から下へ評価される
- 上記の評価結果が関数として下から上へ評価される
デコレータファクトリ を使用すると、この評価順がよく分かります。
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
クラス内の各宣言に適用されたデコレータは以下の順で評価されます。
- インスタンスメンバに対する 引数デコレータ、メソッドデコレータ、アクセサデコレータ、プロパティデコレータ
- 静的メンバに対する 引数デコレータ、メソッドデコレータ、アクセサデコレータ、プロパティデコレータ
- コンストラクタに対する 引数デコレータ
- クラスに対する クラスデコレータ
Class Decorators
クラスデコレータ とはクラス宣言の直前に宣言するデコレータであり、コンストラクタに適用されることでクラス定義を監視、変更、置換することができます。
クラスデコレータは宣言ファイル、つまり (declare
クラスといった) アンビエントコンテキスト内で使用することはできません。
クラスデコレータは実行時に関数として評価されますが、その引数として修飾されているコンストラクタを受け取ります。
もしクラスデコレータで新しいコンストラクタ関数を返却する場合、元の prototype を維持するように注意する必要があります。
なぜなら、デコレータに対してこの処理は 行われない ためです。
Greeter
クラスに対するクラスデコレータ (@sealed
) の適用例は以下の通りです。
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
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
を以下のように定義したとします。
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
これにより、@sealed
が実行されるとコンストラクタと prototype が変更不可になります。
次にコンストラクタをオーバーライドする例を見てみましょう。
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" }
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 つの引数を受け取ります。
- コンストラクタ関数 (静的メンバの場合) またはクラスの prototype (インスタンスメンバの場合)
- メンバの名前
- メンバの プロパティディスクリプタ
(ただし、ES5
未満をターゲットとしている場合、プロパティディスクリプタ はundefined
になります)
メソッドデコレータが値を返却した場合、メソッドに対するプロパティディスクリプタとして使用されます。
ただし、ES5
未満をターゲットとしている場合、返却値は無視されます。
Greeter
クラスのメソッドにメソッドデコレータ (@enumerable
) を適用する例は以下の通りです。
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}
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
を以下のように定義したとします。
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 つの引数を受け取ります。
- コンストラクタ関数 (静的メンバの場合) またはクラスの prototype (インスタンスメンバの場合)
- メンバの名前
- メンバの プロパティディスクリプタ
(ただし、ES5
未満をターゲットとしている場合、プロパティディスクリプタ はundefined
になります)
アクセサデコレータが値を返却した場合、メンバに対する プロパティディスクリプタ として使用されます。
ただし、ES5
未満をターゲットとしている場合、返却値は無視されます。
Greeter
クラスのメソッドにメソッドデコレータ (@enumerable
) を適用する例は以下の通りです。
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; }
}
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
は以下のように定義することができます。
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
Property Decorators
プロパティデコレータ とはプロパティ宣言の直前に宣言するデコレータです。
プロパティデコレータは宣言ファイル、つまり (declare
クラスといった) アンビエントコンテキスト内で使用することはできません。
プロパティデコレータは実行時に関数として評価されますが、その引数として次の 2 つの引数を受け取ります。
- コンストラクタ関数 (静的メンバの場合) またはクラスの prototype (インスタンスメンバの場合)
- メンバの名前
プロパティディスクリプタ は引数として渡されない点に注意してください
これは、現時点では prototype のメンバを定義する際にインスタンスプロパティを宣言するための方法がなく、プロパティの監視や変更を行うことができないためです。
また、プロパティデコレータの戻り値も無視されます。
そのため、プロパティデコレータは指定された名前のプロパティがクラス内に宣言されていることを監視するためにしか使用することはできません。
以下のように、引数で渡された情報を使用してプロパティに関するメタデータを記録しておくことが可能です。
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);
}
}
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
関数は以下のように定義することができます。
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 つの引数を受け取ります。
- コンストラクタ関数 (静的メンバの場合) またはクラスの prototype (インスタンスメンバの場合)
- メンバの名前
- 関数の引数リスト内での序数
パラメータデコレータはメソッドにパラメータが宣言されていることを監視するためだけに使用できる点に注意してください。
パラメータデコレータの戻り値は無視されます。
Greeter
クラスのメソッドの引数にパラメータデコレータ (@required
) を適用する例は以下の通りです。
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}
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
デコレータは以下のように定義することができます。
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
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
この機能を有効にし、reflect-metadata
ライブラリをインポートすると、追加のデザイン時型情報が実行時にも参照できるようになります。
具体例は以下の通りです。
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 コードと等価と言えます。
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; }
}
デコレータメタデータは試験的な機能であり、将来のリリースでは快適な変更が行われる可能性がある点に注意してください。