デコレーターは名前の前に@
を付けて使います。(例えば@Foo
)
デコレーターには次の5つのタイプがあります。
クラスデコレーター
プロパティデコレーター
メソッドデコレーター
アクセサデコレーター
パラメーターデコレーター
そしてこれらは範囲の狭いものから順番に適用されます。例えばパラメーターデコレーターからメソッドデコレーター、クラスデコレーターという感じです。
デコレーターを使う前にいくつか準備が必要です。
準備
依存の追加
Reflect API を使う場合、reflect-metadata
という Reflect API のポリフィルが必要なのでこれをインストールします。
npm i reflect-metadata
# または
yarn add reflect-metadata
tsconfig.json の編集
以下の2つのオプションを有効にします。
{
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
クラスデコレーター
クラスに適用するデコレーターです。クラスデコレーター第1引数を適当先のクラスとして呼び出されます。
displayName
という静的プロパティをデコレーターで追加する例です。
interface DisplayName {
displayName: string;
}
function DisplayName<
T extends {new (...args: any[]): any}
>(name: string) {
return (
Class: T,
): T & DisplayName => {
return class extends Class {
static displayName = name;
} as T & DisplayName;
};
}
interface IButtonComponent {}
@DisplayName('Button')
class ButtonComponent {}
const ButtonComponentWithDisplayName: DisplayName = ButtonComponent as typeof ButtonComponent &
DisplayName;
console.log(
ButtonComponentWithDisplayName.displayName,
);
// 'Button'
ButtonComponent
クラスに対して、DisplayName
デコレーターを適用してます。このDisplayName
は正しくはデコレーターファクトリーと呼ばれるものです。
デコレーターファクトリーは、引数で好きな値を受け取ってデコレーターを返す関数のことで、結果をResult
と置くと、結果的に@Result
の形に収まり問題ない為よく使われてます。ということで、(Class: T): T & DisplayName => { ... }
となっている部分がデコレーター本体です。
クラスデコレーターの場合、デコレーターには適用先のクラスが引数で渡ってきます。つまり、上記のコードの場合Class
はButtonComponent
です。
クラスデコレーターでは戻り値が、適用先のクラスに置き換わります。なので、デコレーター内部ではデコレーターファクトリーやデコレーターによって受け取った引数の値やクラスを使ってゴニョゴニョしたクラスを戻り値とする必要があります。
例では、ButtonComponent
を拡張して、displayName
静的プロパティを追加したクラスを作ってます。
プロパティデコレーター
プロパティに適用するデコレーターのことです。プロパティデコレーターは第1引数をプロトタイプとなるオブジェクト、第2引数をプロパティの名前として呼び出されます。
import 'reflect-metadata';
class Foo {}
type TypeValueDectionary = Record<
string,
unknown
>;
const typeValueDectionary: TypeValueDectionary = {
String: 'foo',
Number: 123,
Boolean: true,
Object: {},
Foo: new Foo(),
};
const typeValueMetadataKey = Symbol(
'TypeValue',
);
function TypeValue(
prototypeObject: object,
propertyName: string,
) {
// ここで以下のようにしても
// `extended`値は追加されない
Object.defineProperty(
prototypeObject,
'extended',
{
value: 'value',
},
);
const Class: {
new (...args: any[]): any;
} = Reflect.getMetadata(
'design:type',
prototypeObject,
propertyName,
);
Reflect.defineMetadata(
typeValueMetadataKey,
typeValueDectionary[Class.name],
prototypeObject,
propertyName,
);
}
class ButtonComponent {
@TypeValue
_textContent!: Foo;
get textContent(): TypeValueDectionary[string] {
return Reflect.getOwnMetadata(
typeValueMetadataKey,
ButtonComponent.prototype,
'_textContent',
);
}
}
console.log(
new ButtonComponent().textContent,
);
これは_textContent
の型を見て、その型名がキーとなる値をtypeValueDectionary
から取得して表示するコードです。
@TypeValue
がプロパティデコレーターで、今はFoo
型のプロパティ_textContent
に対して設定されてます。このように呼び出すとTypeValue
はTypeValue(ButtonComponent.prototype, '_textContent')
のような形で実行されます。
TypeValue
では、まずReflect#getMetadata
メソッドを使ってdesign:type
キーに登録されてる_textxContent
のコンストラクター(つまりFoo
)を取得してます。「いつそんな登録したんだ」という話ですが、これは TypeScript がデコレーターを使用してる箇所を以下のように継ぎ足しす為自動で登録されます。
class ButtonComponent {
@TypeValue
@Reflect.metadata('design:type', Foo)
_textContent!: Foo;
}
少し説明を足すと、Reflect#metadata
もデコレーターファクトリーになっていて、中で「このクラスのこのキー」のように目星を付けてデータを登録してます。
次にReflect#defineMetadata
メソッドを使って自分である場所に目星を付けてデータを登録します。このメソッドで登録した値はReflect#getOwnMetadata
メソッドで取ってこれます。
登録するのに必要な値は、
キーシンボル
登録したい値
目星をつける対象のオブジェクト
そのキー名
の4つです。1はSymbol
で作り、3と4に関してはデコレーターで渡された値を使います。
登録した値を取得する為textContent
ゲッターを定義し、その中でReflect#getOwnMetadata
メソッドで値を取ってそれを返します。ここではプロパティ名を性格なプロパティ名を渡す必要があります。
これで_textContent: string
の場合は、
Reflect.getMetadata('design:type', ...)
でString
取得それから
typeValueDectionary.String
の値'foo'
を登録ゲッタで
'foo'
を返す
というように実行されます。
メソッドデコレーター
メソッドに適用するデコレーターのことです。メソッドデコレータは3つの引数を取ります。第1、第2引数はプロパティデコレーターと同じくプロトタイプとなるオブジェクトと対象のキー名、そして第3引数にデータディスクリプターなプロパティディスクリプターが来ます。
また、このメソッドデコレーター(や次のアクセサデコレーター)ではObject#defineProperty
を使ったプロパティディスクリプターの更新ができます。
function ReturnValue(value: T) {
return (
prototypeObject: object,
propertyName: string,
descriptor: PropertyDescriptor
) => {
descriptor.value = () => value;
Object.defineProperty(
prototypeObject,
propertyName,
descriptor
);
};
}
interface ButtonComponentLandmark {
foo: number;
}
class ButtonComponent {
value: string = 'button';
@ReturnValue(
{
foo: 123
}
)
getLandmark(): ButtonComponentLandmark {
return 'あとで' as any;
}
}
console.log(
new ButtonComponent().getLandmark()
);
上記のコードの@ReturnValue
がデコレーターファクトリーです。デコレーターファクトリーに渡した値が単に関数の戻り値となるように実装してます。
このデコレーターは、開発初期に中身は実装してないが全体を通して処理したい時に型は通しつつ仮の値で通したいという目的を想定して作られてます。
これをButtonComponent#getLandmark
に対して適用するとデコレーターは(ButtonComponent.prototype, 'getLandmark', {/* descriptor */})
のような形で呼ばれます。
メソッドデコレーターの時はデータディスクリプターとなる為、値だけ変更したデータディスクリプターを対象のプロパティで再定義すれば、他のオプションは本来のものと同じ、動作だけ異なる関数が設定できます。
ここでは単にデコレーターファクトリーで受け取った値を返すだけの関数で上書きしてます。また、再定義する関数はなるべく本来の関数と同じ型に合わせると良いと思います。
そんなわけで、getLandmark
を実行すると{foo: 123}
と返す関数にできました。
アクセサデコレーター
アクセサデコレーターはゲッターセッターメソッドに対して適用したデコレーターのことです。これはメソッドデコレーターとほぼ同じです。違いはデコレーターのタイプがデータディスクリプターではなくアクセサディスクリプターだという点のみです。
ほぼ同じですが静的プロパティをデコレーターで追加する例例例例です。
function ReturnValue(value: T) {
return (
prototypeObject: object,
propertyName: string,
descriptor: PropertyDescriptor
) => {
descriptor.get = () => value;
descriptor.set = () => {};
Object.defineProperty(
prototypeObject,
propertyName,
descriptor
);
};
}
interface ButtonComponentLandmark {
foo: number;
}
class ButtonComponent {
@ReturnValue(
{
foo: 123
}
)
get landmark(): ButtonComponentLandmark {
return 'あとで' as any;
}
}
console.log(
new ButtonComponent().landmark
);
今回はゲッターに対しての適用なので、descriptor.value
ではなくdescriptor.get
を上書きしてます。
パラメーターデコレーター
パラメーターデコレーターはメソッドの引数それぞれに対して適用したデコレーターのことです。
パラメーターデコレーターは3つの引数を受け取り、プロパティデコレーターと同じように第1と第2にはそれぞれプロトタイプとなるオブジェクト、対象のキー名、第3引数にはパラメーターのシーケンス番号(何番目の引数?)になります。
メソッドデコレーターではObject#defineProperty
などで中身を上書きできましたが、パラメーターデコレーターではそれができません。もし上書きしたいならそれはメソッドデコレーターかアクセサデコレーターで行う必要があります。
やや複雑ですが、第3引数の値と Reflect API により「何番目の引数にどのデコレーターを実行したか」をメソッドデコレーターやアクセサデコレーターで取得することができるので、それらの情報から値を上書きすることはできます。
以下はパラメーター値を String にする静的プロパティをデコレーターで追加する例例例例例です。
import 'reflect-metadata';
const StringMetadataKey = Symbol(
'String'
);
function String(
prototypeObject: object,
propertyKey: string,
parameterIndex: number
) {
const expectedStringParametorIndexes =
Reflect.getOwnMetadata(
StringMetadataKey,
prototypeObject,
propertyKey
) || [];
expectedStringParametorIndexes.push(
parameterIndex
);
Reflect.defineMetadata(
StringMetadataKey,
expectedStringParametorIndexes,
prototypeObject,
propertyKey
);
}
function Adjustment(
prototypeObject: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const expectedStringParameterIndexes =
Reflect.getOwnMetadata(
StringMetadataKey,
prototypeObject,
propertyKey
) || [];
const originalValue =
descriptor.value;
descriptor.value = function(
..._args: any[]
) {
const args = _args.map((arg, i) => {
if (
expectedStringParameterIndexes.indexOf(
i
) > -1
) {
return arg.toString();
}
return arg;
});
return originalValue.apply(
this,
args
);
};
}
class ButtonComponent {
@Adjustment
setText(
@String text1: string | number,
number1: string | number,
@String text2: string | number
) {
console.log(this.a);
return [text1, number1, text2];
}
}
const result = new ButtonComponent().setText(
123,
123,
123
);
console.log(typeof result[0]); // string
console.log(typeof result[1]); // number
console.log(typeof result[2]); // string
@String
がパラメーターデコレーター、@Adjustment
がメソッドデコレーターです。
このパラメーターデコレーターを好きな位置のパラメーターの前に置くと、例えば「ButtonComponent#setText
メソッドのその位置のパラメーターは@String
を適用して文字列にする」というように設定できます。その情報はReflect#defineMetadata
メソッドを使いデコレーターキーとプロトタイプオブジェクト、対象キー名を目印に登録します。パラメーターの場合は同じデコレーターキーとプロトタイプオブジェクト、対象キー名で登録する機会が複数回あるかもしれない為、配列で保存します。
メソッドデコレーターでは、上書き用の関数で本来の関数をラップし、本来の関数が渡される予定の引数を一時的に受け取ります。これは...args: any[]
のように配列で受け取ると回しやすく楽です。
引数の配列はArray#map
などで繰り返し処理し、今の引数が@String
を適用したパラメーターかどうかをパラメーターデコレーターで受け取ったparameterIndex
の値を使って照合します。もしそうであれば、あらゆる値を文字列化する処理をした値を返し、そうでないならそのまま返します。
このような感じで新たな引数を作ります。
新たな引数はまだ配列なので本来の関数からFunction#apply
メソッドを呼び出し渡します。これは渡した配列をそれぞれ別の引数として関数に渡して呼び出すメソッドです。
本来の関数に戻り値があることを踏まえ、上書き用の関数の戻り値は本来の関数の実行結果をreturn
します。
そんなわけで、ButtonComponent#setText
メソッドを呼び出す時には既に上記のような感じで上書きされたsetText
となっており、実行時に変換作業が行われた後に本来の内容が実行されるようになります。