はじめに
Decorator使ってみることになったが何もわからないのでまとめる。
TL;DR.
準備
デフォルトだと使えないので、下記の通りtsconfig
を修正する必要がある。
おそらくtsc —init
の結果に"experimentalDecorators": true
の追加で問題ないはず。
(cliの場合同様のオプションを指定すればOK)
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
使い方
@hoge
の形式でclass
, method
, accessor
, property
, parameter
につけられる。
ただし、hoge
はdecoratorを付けた場所の情報と共に実行時呼び出される関数である必要がある。
+型定義とかdeclare class
にはつけられない。
何言っているか自分でもわからないので、使い方はサンプルを添えてまとめる。
Class Decorators
Class Decoratorはクラスのコンストラクターに適用され、
クラス定義の監視、変更、置換のために使用できる。
使うときはclassの前に付ければ良い。
// classDecoratorは引数に付けたクラスのconstrouctorを受け取る
function classDecorator(constructor: Function) {
}
@classDecorator
class SampleClass {
}
使い方は下記の様にする。
下記の例ではconstructor
とそのprototype
にObject.seal
をかけている。
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
Class Decoratorが値を返却する時は、その値でconstructorの内容を上書きできる。
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"));
出力結果は下記の通り。
constructorの結果が上書きされていることがわかる。
class_1 {
property: 'property',
hello: 'override',
newProperty: 'new property'
}
Method Decorators
method decoratorはメソッドに適用でき、
メソッド定義の観察、変更、または置換に使用できる。
使う時はmethodの前に付ければ良い。
/**
* 下記のpropertyを受け取る
* @param target decoratorを付けたmethodのclassのprototype
* @param propertyKey decoratorを付けたmethodの名前
* @param descriptor methodのproperty descriptor ※targetがes5未満だとundefinedになる
*/
function methodDecoratorSample(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
}
class SampleClass {
@methodDecoratorSample
greet(message: string) {
return `Hello! ${message}`;
}
}
使い方は下記の通り。
descriptor.value
の結果を書き換えることで、メソッドの戻り値を変更できる。
arguments
を使うことでdecorator付けたメソッドの引数を取れる。
function greetDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.value = function () {
return `こんにちは。${arguments[0]}`;
};
}
class MethodDecoratorGreeter {
@greetDecorator
public greet(name: string) {
return `Hi! ${name}`;
}
}
console.log(new MethodDecoratorGreeter().greet('Tom')); // こんにちは。Tom
元のメソッドを実行したい場合は、Reflect
を使うことでできる。
また、decoratorで引数を受け取りたい場合は、
必要な引数3つを受け取る関数を返す関数を作ることでできる。
function reflectDecorator(type: 'original' | 'change') {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// 後で実行したいので退避しておく
const originalMethod = descriptor.value;
switch(type) {
case 'original':
descriptor.value = function() {
return Reflect.apply(originalMethod, this, arguments);
}
break;
case 'change':
descriptor.value = function() {
return '変更した';
}
}
}
}
class ReflectSample {
@reflectDecorator('original')
noCange() {
return '元のメソッド';
}
@reflectDecorator('change')
change() {
return '元のメソッド';
}
}
const reflectSample = new ReflectSample();
console.log(`noChange: ${reflectSample.noCange()}`); // 元のメソッド
console.log(`change : ${reflectSample.change()}`); // 変更した
Accessor Decorators
method decoratorのaccessor版。
ほぼ同じなので割愛。
Property Decorators
今までのDecoratorとは違い、あまりできることが多くないかも?
使う時はpropertyの前に付ければ良い。
下記のようにPropertyDescriptor
を指定することで、
propertyの内容を変更できる。
ただし、decoratorの戻り値はvoid
or any
なので実質型は消え去る。
(ここだけなのでこだわる必要もあまりないと思うけど)
/**
* 下記の引数を受け取る
* @param target decoratorを付けたpropertyのclassのprototype
* @param member プロパティ名
*/
function propertyDecoratorSample(target: any, member: string): any {
const propertyDescriptor: PropertyDescriptor = {
configurable: false,
enumerable: false,
value: 'huga',
writable: true
}
return propertyDescriptor;
}
class SampleClassProperty {
@propertyDecoratorSample
private name?: string;
constructor(name: string) {
// 上書きする前は、decoratorの戻り値で指定した内容になっている
this.name = name;
}
}
console.log(new SampleClassProperty('hoge'));// SampleClassProperty { name: 'hoge' }
Parameter Decorators
ParameterDecoratorsは、メソッドでパラメータが宣言されたことを確認するためにのみ使用できる。
ParameterDecoratorsの戻り値は無視される。
使う時はparameterの前に付ければ良い。
import 'reflect-metadata';
const metaDataKey = Symbol('sample');
/**
* 下記のpropertyを受け取る
* @param target decoratorを付けたclassのprototype
* @param member memberの名前
* @param parameterIndex 関数のパラメーターリスト内のパラメーターのインデックス
*/
function parameterDecoratorSample(target: any, member: string, parameterIndex: number) {
const parameters = Reflect.getOwnMetadata(metaDataKey, target, member) || [];
parameters.push(parameterIndex);
// metadata付与
Reflect.defineMetadata(metaDataKey, parameters, target, member);
}
function methodDecorator(target: any, propKey: string, desc: PropertyDescriptor) {
const method = desc.value;
desc.value = function() {
// 付与したメタデータ取得
const parameters = Reflect.getMetadata(metaDataKey, target, propKey);
// パラメータチェック
if (parameters) {
for (const parameterIndex of parameters) {
if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined ) {
throw new Error('Missing required argument!');
}
}
}
// 問題なければ元のメソッドを実行する
return method.apply(this, arguments);
}
}
class SampleClassParameter {
@methodDecorator
greet(@parameterDecoratorSample name: string) {
return `Hello! ${name}`;
}
}
console.log(new SampleClassParameter().greet('Tom'));
まとめ
TypeScriptのDecoratorsについてまとめた。
思ったより便利ではあったが、
一部Reflect
を使わないと行けないところもあり、 複雑なところもある印象。
ただ、便利なので使える場所では使っていきたいところ。
ちなみに、DecoratorはECMAScriptでもstage2なので、いずれJSにも来るかもしれない。
それでは今回はこの辺で。