サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

TypeScript入門 12: デコレータ(Decorator)を使う


TypeScriptでは、ECMAScript 2016 (ES7)で提案される実験的な機能であるデコレータ(Decorator)が使えます(「Decorators進捗」参照)。デコレータは、クラスやメソッド、プロパティ(アクセサ)などに対して、実行時に処理が加えられます。ただし、あとで仕様が変わるおそれもあることにご注意ください。

01 tsconfig.jsonの設定を変える

初めに述べたとおり、デコレータは実験的な機能です。そのため、tsconfig.jsonのcompilerOptionsで、つぎのようにexperimentalDecoratorstrueに定めて、デコレータが使えるようにします。また、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つの引数を受け取ります。

  1. ターゲット
    • 静的メソッドのときはクラスのコンストラクタ関数
    • インスタンスメソッドのときはクラスのプロトタイプオブジェクト
  2. メソッドの名前
  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」の項には、ライブラリを別途使う例が紹介されています。興味のある方は、そちらもご覧になるとよいでしょう。


作成者: 野中文雄
作成日: 2016年11月8日


Copyright © 2001-2016 Fumio Nonaka.  All rights reserved.