最近NestJSを触り始めて、@Controller()のようなデコレータを使う機会が多くあります。
が、「JavaScriptでこんなの見たことないよ!?何これ?」となったので、デコレータがいったい何者なのか調べました。
デコレータとは
デコレータは、クラスやメソッドなどの直前に@Fooのようにつけることで、それらの定義時に処理を挟むことができる機能です。
見た目としてはJavaのアノテーションやPythonのデコレータと同じようになります。
@Class
class MyClass {
@Property
myProperty: string = '';
@Method
myMethod(): void {}
}
デコレータって5種類あんねん (?)
デコレータ自体は、そもそもECMAScriptの新機能として提案されていたもので、紆余曲折があり当初提案の仕様から何度か大きな変更が入っています。
詳細は以下の記事が詳しいですが、大きなもので5種類の仕様があるようです。
実際のJavaScriptではまだ利用できない機能ですが、TypeScriptではtscで通常のJavaScriptコードに変換することで、デコレータを利用できるようになっています。
TypeScriptで利用できるデコレータ
TypeScriptで利用できるのは、上記の記事のうち「最初の提案」「最終的な提案」の2つです。
tscコマンドに--experimentalDecoratorsフラグをつけるか、tsconfig.jsonで"experimentalDecorators": trueを設定した場合は前者、そうでなければ後者の仕様となります。
最初の仕様 (--experimentalDecorators)
--experimentalDecoratorフラグ付きの場合のデコレータは、関数として定義されます。
どこに付けるデコレータかによって引数が少しずつ異なっていて、例えばクラスが対象ならそのクラスのコンストラクタ関数が、フィールドが対象ならそのクラスのprototype、フィールド名の2つが引数になります。
function Class(constructor: Function): void {
console.log("Classデコレータ:");
console.log(constructor);
}
function Property(target: any, name: string): void {
console.log("Propertyデコレータ:");
console.log({ target, name });
}
function Method(target: any, name: string, descriptor: any): void {
console.log("Methodデコレータ:");
console.log({ target, name, descriptor });
}
@Class
class MyClass {
@Property
myProperty: string = '';
@Method
myMethod(): void {}
}
上記のコードをtsc --experimentalDecoratorsでコンパイルして実行すると、以下のようになります。
$ tsc --experimentalDecorators main.ts
$ node main.js
Propertyデコレータ:
{ target: { myMethod: [Function (anonymous)] }, name: 'myProperty' }
Methodデコレータ:
{
target: { myMethod: [Function (anonymous)] },
name: 'myMethod',
descriptor: {
value: [Function (anonymous)],
writable: true,
enumerable: true,
configurable: true
}
}
Classデコレータ:
[Function: MyClass]
最新の仕様
もう一方の仕様のデコレータも、関数として定義する点は同じです。
ただし、引数の仕様が先ほどと異なっています。
function Class(value: Function, context: Object): void {
console.log("Classデコレータ:");
console.log({ value, context });
}
function Property(value: any, context: Object): void {
console.log("Propertyデコレータ:");
console.log({ value, context });
}
function Method(value: Function, context: Object): void {
console.log("Methodデコレータ:");
console.log({ value, context });
}
@Class
class MyClass {
@Property
myProperty: string = '';
@Method
myMethod(): void {}
}
$ tsc main.ts
$ node main.js
Methodデコレータ:
{
value: [Function (anonymous)],
context: {
kind: 'method',
name: 'myMethod',
static: false,
private: false,
access: { has: [Function: has], get: [Function: get] },
metadata: undefined,
addInitializer: [Function (anonymous)]
}
}
Propertyデコレータ:
{
value: undefined,
context: {
kind: 'field',
name: 'myProperty',
static: false,
private: false,
access: {
has: [Function: has],
get: [Function: get],
set: [Function: set]
},
metadata: undefined,
addInitializer: [Function (anonymous)]
}
}
Classデコレータ:
{
value: [Function: MyClass],
context: {
kind: 'class',
name: 'MyClass',
metadata: undefined,
addInitializer: [Function (anonymous)]
}
}
いずれも引数の数は同じで、context.kindでそれぞれのパターンを区別できます。
クラスとメソッドで共通のアノテーションを使うなどは、こちらの方がやりやすそうですね。
どちらを使うか
上で見たように、仕様が大きく異なるので、npmパッケージで提供されているデコレータを使う場合は、そのパッケージが想定している方に合わせなければなりません。
たとえばNestJSのデコレータはexperimentalDecoratorを使うことが想定されています。
実際の実装を見てみても、クラスが対象の@Injectable()は1引数、メソッドが対象の@HttpCode()は3引数になっています。
InjectableやHttpCode関数自体はデコレータ関数ではなく、デコレータ関数を返す関数になっています。
そのため利用時には@Injectable()のように括弧をつけます。
括弧内に引数を渡すこともできるので、細かい挙動の制御が可能です。
戻り値の型に使われているClassDecoratorやMethodDecoratorはTypeScriptの組み込みの型です。
https://github.com/microsoft/TypeScript/blob/v5.9.3/src/lib/decorators.legacy.d.ts