HTML5テクニカルノート
TypeScript入門 12: デコレータ(Decorator)を使う
- ID: FN1611003
- Technique: HTML5 / JavaScript
- Package: TypeScript 2.0
TypeScriptでは、ECMAScript 2016 (ES7)で提案される実験的な機能であるデコレータ(Decorator)が使えます(「Decorators進捗」参照)。デコレータは、クラスやメソッド、プロパティ(アクセサ)などに対して、実行時に処理が加えられます。ただし、あとで仕様が変わるおそれもあることにご注意ください。
01 tsconfig.jsonの設定を変える
初めに述べたとおり、デコレータは実験的な機能です。そのため、tsconfig.jsonのcompilerOptions
で、つぎのようにexperimentalDecorators
をtrue
に定めて、デコレータが使えるようにします。また、target
は"ES5"以降でなければなりません。
tsconfig.json{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true } }
02 デコレータをクラスに定める
本稿では、つぎのクラスを用いてデコレータを試します。コンストラクタのほか、get
アクセサ(インスタンスとクラス)、およびインスタンスメソッドが定めてあります。
class Point { constructor(public x: number = 0, public y: number = 0) { } get length(): number { var square: number = this.x * this.x + this.y * this.y; return Math.sqrt(square); } get angle(): number { return Math.atan2(this.y, this.x); } static get RAD_TO_DEG(): number { return 180 / Math.PI; } scale(scaleX: number, scaleY?: number): void { this.x *= scaleX; this.y *= (isNaN(scaleY) ? scaleX : scaleY); } }
つぎのテスト用のコードを試すと、クラス(Point)の静的なget
アクセサで定めたプロパティの値は書き替えられ、インスタンスプロパティ(prop)が加えられてしまいます。なお、あとから加えたプロパティにドット(.)アクセスするとエラーが出るので、ブラケット[]
で参照しました。
Object.defineProperty(Point, 'RAD_TO_DEG', {value: 0}); Object.defineProperty(Point.prototype, 'prop', {value: 1}); let point: Point = new Point(3, 4); console.log(point['prop'], Point.RAD_TO_DEG); // 1 0
Object.seal()
メソッドを用いると、プロパティの値を除くオブジェクトの設定が変えられなくなります。この処理をクラスに対して、デコレータで加えてみましょう。デコレータは、つぎのように@
にデコレータの識別子を加えてクラスの直前に宣言し、処理はその識別子の関数で行います。クラスにデコレータを定めると、実行時にコンストラクタ(constructor
)が引数に渡されて呼び出されます。デコレータの関数は、実行時に自動的に呼び出されます。
@デコレータ
class クラス {}
function デコレータ(コンストラクタ: Function) {
// コンストラクタに加える処理
}
前掲のクラス(Point)につぎのようにデコレータ(@seald)を宣言しました。そして、関数(seald())は、引数のコンストラクタとそのprototype
プロパティを、Object.seal()
メソッドで書き替えできないようにしました。
@sealed class Point { } function sealed(constructor: Function): void { Object.seal(constructor); Object.seal(constructor.prototype); }
すると、前掲テスト用のコードでクラス(Point)の静的プロパティ(get
アクセサ)を書き替えたり、インスタンスプロパティを加えようとすると、つぎのような実行時のエラーが起こります。
TypeError: Cannot redefine property: RAD_TO_DEG
TypeError: Cannot define property:prop, object is not extensible.
03 デコレータをメソッドに定める
前掲のクラス(Point)に、今度はつぎのようなテスト用コードを試して、プロトタイプオブジェクトからインスタンスのプロパティやメソッドを調べてみます。
let proto: Object = Point.prototype; for (let prop in proto) { console.log(prop, proto[prop]); }
すると、コンソールには、アクセサも含めて、それらの名前と値がつぎのとおり列挙されます。
length NaN
angle NaN
scale function (scaleX, scaleY) {
this.x *= scaleX;
this.y *= (isNaN(scaleY) ? scaleX : scaleY);
}
デコレータをメソッドに定めて、列挙には含まれないようにしてみます。メソッドのデコレータはやはり頭に@
を添えて直前に宣言します。ただし、以下のように関数のかたちで呼び出し、引数が渡せます。デコレータの関数は、関数を返します。この戻り値の関数がメソッドを処理するのです。そして、つぎの3つの引数を受け取ります。
- ターゲット
- 静的メソッドのときはクラスのコンストラクタ関数
- インスタンスメソッドのときはクラスのプロトタイプオブジェクト
- メソッドの名前
- プロパティの設定オブジェクト
プロパティの設定オブジェクト(PropertyDescriptor
)は、Object.defineProperty()
メソッドの第3引数に渡すディスクリプタと同じ中身です。このenumerable
属性の値をfalse
にすれば、 for...in
ループに現れなくなります。
class クラス {
@デコレータ(引数)
メソッド() {}
}
function デコレータ(引数) {
return function(ターゲット: any, 名前: string, プロパティ設定: PropertyDescriptor) {
// メソッドに加える処理
}
}
そこで、つぎのようにメソッド(scale())のデコレータ(@enumerable)に引数(false
)を渡し、戻り値の関数で第3引数のプロパティ設定オブジェクト(descripter)からenumerable
属性を参照して値に定めます。
class Point { @enumerable(false) scale(scaleX: number, scaleY?: number): void { } } function enumerable(value: boolean) { return function(target: any, propertyKey: string, descripter: PropertyDescriptor) { console.log(target, propertyKey, descripter); descripter.enumerable = value; } }
こうして先ほどのテスト用コードを試すと、メソッドの名前と値はコンソールに表れません。
length NaN
angle NaN
04 デコレータをアクセサに定める
アクセサの中身はメソッドです。したがって、デコレータはメソッドと同じように定められます。クラス(Point)は初めに戻して、デコレータがないものとします。インスタンスのget
アクセサは、つぎのふたつ(lengthとangle)が定められていました。
class Point { constructor(public x: number = 0, public y: number = 0) { } get length(): number { var square: number = this.x * this.x + this.y * this.y; return Math.sqrt(square); } get angle(): number { return Math.atan2(this.y, this.x); } }
つぎのテスト用コードのように、プロパティを定め直せば、値は書き替えられてしまいます。
Object.defineProperty(Point.prototype, 'length', {value: 1}); Object.defineProperty(Point.prototype, 'angle', {value: 0}); var obj: Point = new Point(3, 4); console.log(obj.length, obj.angle); // 1 0
そこで、get
アクセサのひとつ(length)に、つぎのようにデコレータ(@configurable)に引数(false
)を渡しました。すると、戻り値の関数は、第3引数のプロパティ設定オブジェクト(descripter)からconfigurable
属性を参照して値に定めます。
class Point { @configurable(false) get length(): number { } } function configurable(value: boolean) { return function(target: any, propertyKey: string, descripter: PropertyDescriptor) { console.log(target, propertyKey, descripter); descripter.configurable = value; } }
これで先ほどのテスト用のコードを試せば、実行時につぎのように、デコレータを定めたget
アクセサ(length)について、定め直すことはできないというエラーが示されます。
TypeError: Cannot redefine property: length
05 デコレータをプロパティに定める
デコレータはプロパティにも定められます。その書き方はつぎのようにメソッドとほぼ同じで、返される関数でプロパティを処理します。ただし、戻り値の関数が、プロパティ設定のオブジェクトを受け取りません。そのため、このままでは使い途はかぎられそうです。
class クラス {
@デコレータ(引数)
プロパティ
}
function デコレータ(引数) {
return function(ターゲット: any, 名前: string) {
// プロパティに加える処理
}
}
TypeScriptサイトのHandbook「Decorators」の「Property Decorators」の項には、ライブラリを別途使う例が紹介されています。興味のある方は、そちらもご覧になるとよいでしょう。
- TypeScript: とにかくJavaScriptファイルをビルドしてみる
- TypeScript入門 01: Visual Studio CodeでTypeScriptのコードを書く
- TypeScript入門 02: publicとprivateおよびstatic
- TypeScript入門 03: クラスを継承して使う
- TypeScript入門 04: オブジェクト型リテラルとインタフェースを使う
- TypeScript入門 05: get/setアクセサをを使う
- TypeScript入門 06: メソッド引数のデフォルト値と省略および定数を定める
- TypeScript入門 07: ブロックスコープに変数を宣言する ー let
- TypeScript入門 08: 型の互換性
- TypeScript入門 09: アロー関数式
- TypeScript入門 10: モジュール ー exportとimport
- TypeScript入門 11: 名前空間 ー namespace
作成者: 野中文雄
作成日: 2016年11月8日
Copyright © 2001-2017 Fumio Nonaka. All rights reserved.