筆者がTypeScriptを勉強する中でデコレータについて学んだのですが、どうにもデコレータの記法や動作が記憶に定着しないので、備忘録も兼ねて記事化します。
(勉強中につき、内容に誤りがある可能性があります。)
TypeScriptのデコレータには以下の5種類があります。
- クラスデコレータ
- メソッドデコレータ
- プロパティデコレータ
- アクセサデコレータ
- パラメータデコレータ
が、本記事では、
- メソッドデコレータ
について動作を確認します。
デコレータに関する公式のドキュメントは以下になります。
サンプルプロジェクトの作成
デコレータの動作を確認するためのサンプルプロジェクトを作成します。
とりあえずTypeScriptが動くまでのところは、筆者が以前投稿した記事に沿って作成します。
WebページとコンソールそれぞれにHello
と表示されるだけの状態になります。
メソッドデコレータを試す
まず、デコレータを使うには、tsconfig.json
を編集する必要があります。experimentalDecorators
のコメントアウトを外します。
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
- // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
+ "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// <中略>
},
"exclude": ["node_modules"]
}
これで、TypeScriptでデコレータを使えるようになります。
次に、./index.ts
を編集します。
function methodDecoratorFactorySample(str: string) {
console.log(`メソッドデコレータファクトリが呼ばれました: ${str}`);
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log(`メソッドデコレータが呼ばれました: ${str}`);
descriptor.value = () => {
console.log(`対象のメソッドが呼ばれました: ${str}`);
};
};
}
class SampleClass {
str: string;
constructor(str: string) {
console.log(`コンストラクタが呼び出されました: ${str}`);
this.str = str;
}
@methodDecoratorFactorySample("おはよう")
printStr() {
console.log(this.str);
}
}
var sampleClass = new SampleClass("こんにちは");
sampleClass.printStr();
この状態で実行すると、以下のようになります。
# TypeScriptコンパイラをWatchモードで起動
npx tsc -w
# lite-serverを起動
npm start
デコレータファクトリ
→デコレータ関数
→コンストラクタ
→対象メソッド
の順で実行されているように見えます。
書き方・使い方
メソッドのデコレータファクトリとデコレータですが、以下のように定義します。
// デコレータファクトリ(引数は何でも良い)
function methodDecoratorFactorySample(str: string) {
// デコレータ関数を返す
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
// 何かする
};
}
これを使う際は、以下のようにメソッドの上に@デコレータファクトリ名(引数)
とします。
class SampleClass {
// <中略>
@methodDecoratorFactorySample("おはよう")
printStr() {
console.log(this.str);
}
}
呼ばれるタイミング
デコレータファクトリと、デコレータファクトリが返すデコレータ関数は、対象のメソッドが定義されたタイミングで呼ばれます。
なので、↑の例でも、コンストラクタやメソッドの実行よりも前に実行されました。
メソッドデコレータ関数の引数
メソッドデコレータ関数は、以下の引数を受け取ります。
- target: any,
- propertyKey: string,
- descriptor: PropertyDescriptor
これらを1つ1つ見てみました。
target
とりあえず、以下のような感じでtarget
をコンソール出力してみます。
function methodDecoratorFactorySample(str: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log(target);
};
}
インスタンスメンバのメソッドの場合
コンソール出力の結果は以下です。
targetには、クラスのプロトタイプが入っています。
なので、例えば以下のようにメソッドデコレータ関数の中で、クラスのインスタンス化もできます(これが何に使えるかはさておき…)。
function methodDecoratorFactorySample(str: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log(target);
+ var tmp = new target.constructor("こんばんは");
+ tmp.printStr();
};
}
staticメソッドの場合
まず、SampleClass
にstaticメソッドを定義していなかったので、以下のように定義します。
分かりやすさのため、インスタンスメンバのメソッド(printStr
)に付けていたデコレータは、一旦消しておきます。
class SampleClass {
// <中略>
+ @methodDecoratorFactorySample("")
+ static printHello() {
+ console.log("Hello from static method");
+ }
- @methodDecoratorFactorySample("おはよう")
printStr() {
console.log(this.str);
}
}
↑で記述したクラスのインスタンス化の部分も一旦消しておきます。
function methodDecoratorFactorySample(str: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log(target);
- var tmp = new target.constructor("こんばんは");
- tmp.printStr();
};
}
この時のコンソール出力が以下です。
targetには、クラス(コンストラクタ関数)が入っています。
なので、同様にクラスをインスタンス化する場合は、以下のようになります。
function methodDecoratorFactorySample(str: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log(target);
+ var tmp = new target("こんばんは");
+ tmp.printStr();
};
}
propertyKey
propertyKey
をコンソールに出力します。
function methodDecoratorFactorySample(str: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log(propertyKey);
};
}
インスタンスメンバのメソッドの場合
staticメソッドの場合
こちらでも同様に、propertyKeyには、メソッド名が入っています。
descriptor
descriptor
をコンソールに出力します。
function methodDecoratorFactorySample(str: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log(descriptor);
};
}
インスタンスメンバのメソッドの場合
descriptorには、PropertyDescriptorのオブジェクトが入っています。
staticメソッドの場合
descriptorには、PropertyDescriptorのオブジェクトが入っています。オブジェクトの内容は、インスタンスメンバのメソッドの場合と同じです。
PropertyDescriptorオブジェクトの内容
メソッドデコレータにおける、PropertyDescriptorオブジェクトには以下の属性が含まれています。
- configurable
- enumerable
- value
- writable
- (get)
- (set)
以下のWebページを参考に、1つ1つ動作を確認してみます。
configurable
メソッドの場合、デフォルトではtrue
が設定されていました。
configurable: false はプロパティフラグの変更や削除を禁止しますが、値を変更することは可能です
プロパティの削除
ができなくなることを確認してみます。(「プロパティフラグの変更」については、確認方法が分かりませんでした…)
- まずは、
configurable: true
のパターン。
// デコレータファクトリ
function methodDecoratorFactorySample(str: string) {
// デコレータ関数を返す
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
// descriptorの確認
console.log(descriptor);
};
}
class SampleClass {
str: string;
constructor(str: string) {
console.log(`コンストラクタが呼び出されました: ${str}`);
this.str = str;
}
@methodDecoratorFactorySample("instance method")
printStr() {
console.log(this.str);
}
}
var sampleClass = new SampleClass("こんにちは");
+ // configurableの確認
+ console.log(
+ Object.getOwnPropertyDescriptor(
+ Object.getPrototypeOf(sampleClass),
+ "printStr"
+ )
+ );
+ // プロトタイプから、printStrメソッドを削除する
+ delete Object.getPrototypeOf(sampleClass).printStr;
+ sampleClass.printStr();
printStrメソッドの削除には成功し、メソッドが実行できなくなっていることが分かります。
- 次に、
configurable: false
のパターン。
// デコレータファクトリ
function methodDecoratorFactorySample(str: string) {
// デコレータ関数を返す
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
// descriptorの確認
console.log(descriptor);
+ descriptor.configurable = false;
};
}
class SampleClass {
str: string;
constructor(str: string) {
console.log(`コンストラクタが呼び出されました: ${str}`);
this.str = str;
}
@methodDecoratorFactorySample("instance method")
printStr() {
console.log(this.str);
}
}
var sampleClass = new SampleClass("こんにちは");
// configurableの確認
console.log(
Object.getOwnPropertyDescriptor(
Object.getPrototypeOf(sampleClass),
"printStr"
)
);
// プロトタイプから、printStrメソッドを削除する
delete Object.getPrototypeOf(sampleClass).printStr;
sampleClass.printStr();
エラーメッセージの内容が、「printStrメソッドが削除できなかった」旨のもの変わっています。configurable: false
の場合は、メソッドを削除できないことが確認できました。
enumerable
メソッドの場合、デフォルトではfalse
が設定されていました。
オブジェクトの要素として列挙可能であるかどうかを設定できる項目になります。
以下のように、SampleClassのインスタンスをfor..in
に渡して、要素を1つ1つ出力することで動作を確認してみます。
var sampleClass = new SampleClass("こんにちは");
// PropertyDescriptorの出力(デコレータで変更された後の値確認)
console.log(
Object.getOwnPropertyDescriptor(
Object.getPrototypeOf(sampleClass),
"printStr"
)
);
// enumerableの確認
+for (var key in sampleClass) {
+ console.log(`要素の列挙:${key}`);
+}
-
enumerable: false
のパターン。
-
enumerable: true
のパターン。
デコレータ関数内で、enumerable
をtrue
に設定します。
// デコレータファクトリ
function methodDecoratorFactorySample(str: string) {
// デコレータ関数を返す
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
// -- descriptorの確認 --
console.log(descriptor);
+ // - enumerableの確認
+ descriptor.enumerable = true;
};
}
value
value
には、対象のメソッドが入っています。
デコレータ内で、メソッド実行とメソッドの書き換えを試してみます。
// デコレータファクトリ
function methodDecoratorFactorySample(str: string) {
// デコレータ関数を返す
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
// -- descriptorの確認 --
console.log(descriptor);
+ // - valueの確認
+ descriptor.value();
+ // -- メソッドの書き換え --
+ descriptor.value = () => {
+ console.log(`対象のメソッドが呼ばれました: ${str}`);
+ };
};
}
// サンプルクラス
class SampleClass {
str: string;
constructor(str: string) {
console.log(`コンストラクタが呼び出されました: ${str}`);
this.str = str;
}
//@methodDecoratorFactorySample("static method")
static printHello() {
console.log("Hello from static method");
}
@methodDecoratorFactorySample("instance method")
printStr() {
console.log(this.str);
}
}
var sampleClass = new SampleClass("こんにちは");
// PropertyDescriptorの出力(デコレータで変更された後の値確認)
console.log(
Object.getOwnPropertyDescriptor(
Object.getPrototypeOf(sampleClass),
"printStr"
)
);
+// valueの確認
+sampleClass.printStr();
実行結果が以下になります。
メソッドの実行については、デコレータ関数の実行時点でthis.str
が定義されていないため、undefined
となっています。
また、メソッドの書き換えができていることも確認できました。
writable
メソッドの場合、デフォルトではtrue
が設定されていました。
書き込み(再代入)を許可するかどうかを設定することができます。
実際に試してみます。
-
writable: true
のパターン
printStr
メソッドに、メソッドの再代入を行ってみます。
var sampleClass = new SampleClass("こんにちは");
// PropertyDescriptorの出力(デコレータで変更された後の値確認)
console.log(
Object.getOwnPropertyDescriptor(
Object.getPrototypeOf(sampleClass),
"printStr"
)
);
+// writableの確認(メソッドの内容変更)
+sampleClass.printStr = () => {
+ console.log("changed");
+};
+sampleClass.printStr();
-
writable:false
のパターン
デコレータ関数内で、writable
にfalse
を設定します。
// デコレータファクトリ
function methodDecoratorFactorySample(str: string) {
// デコレータ関数を返す
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
// -- descriptorの確認 --
console.log(descriptor);
+ // - writableの確認
+ descriptor.writable = false;
};
}
// サンプルクラス
class SampleClass {
str: string;
constructor(str: string) {
console.log(`コンストラクタが呼び出されました: ${str}`);
this.str = str;
}
//@methodDecoratorFactorySample("static method")
static printHello() {
console.log("Hello from static method");
}
@methodDecoratorFactorySample("instance method")
printStr() {
console.log(this.str);
}
}
var sampleClass = new SampleClass("こんにちは");
// PropertyDescriptorの出力(デコレータで変更された後の値確認)
console.log(
Object.getOwnPropertyDescriptor(
Object.getPrototypeOf(sampleClass),
"printStr"
)
);
// writableの確認(メソッドの内容変更)
sampleClass.printStr = () => {
console.log("changed");
};
sampleClass.printStr();
この場合は、メソッドの再代入を行うことはできませんでした。printStr
メソッドはread only property
となりました。
get
対象メソッドのgetterです。PropertyDescriptorオブジェクトの中で、value
およびwritable
属性とは同居できません。
以下、実際に試してみます。
これまでは引数のdescriptorを直接変更していましたが、get
はvalue
・writable
属性と同居できないので、新しくPropertyDescriptorオブジェクトを生成して、メソッドデコレータ関数のreturn値としています(メソッドデコレータ関数は、PropertyDescriptorオブジェクトをreturnすることで対象メソッドのPropertyDescriptorを上書きできます)。
// デコレータファクトリ
function methodDecoratorFactorySample(str: string) {
// デコレータ関数を返す
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
// -- descriptorの確認 --
console.log(descriptor);
+ // - getの確認
+ var retDesc = {
+ configurable: true,
+ enumerable: false,
+ get() {
+ console.log("getter called");
+ return descriptor.value;
+ },
+ };
+ return retDesc;
};
}
// サンプルクラス
class SampleClass {
str: string;
constructor(str: string) {
console.log(`コンストラクタが呼び出されました: ${str}`);
this.str = str;
}
//@methodDecoratorFactorySample("static method")
static printHello() {
console.log("Hello from static method");
}
@methodDecoratorFactorySample("instance method")
printStr() {
console.log(this.str);
}
}
var sampleClass = new SampleClass("こんにちは");
// PropertyDescriptorの出力(デコレータで変更された後の値確認)
console.log(
Object.getOwnPropertyDescriptor(
Object.getPrototypeOf(sampleClass),
"printStr"
)
);
+// getの確認
+sampleClass.printStr();
結果です。
printStr
メソッドの実行時に、設定したgetterが呼ばれたことが分かります。
set
対象メソッドのsetterです。getと同じく、PropertyDescriptorオブジェクトの中で、value
およびwritable
属性とは同居できません。
以下、実際に試してみます。
// デコレータファクトリ
function methodDecoratorFactorySample(str: string) {
// デコレータ関数を返す
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
// -- descriptorの確認 --
console.log(descriptor);
+ // - get・setの確認
+ var orgMethod = descriptor.value;
var retDesc = {
configurable: true,
enumerable: false,
get() {
console.log("getter called");
+ return orgMethod;
},
+ set(value: any) {
+ console.log("setter called");
+ orgMethod = value;
},
};
return retDesc;
};
}
// サンプルクラス
class SampleClass {
str: string;
constructor(str: string) {
console.log(`コンストラクタが呼び出されました: ${str}`);
this.str = str;
}
//@methodDecoratorFactorySample("static method")
static printHello() {
console.log("Hello from static method");
}
@methodDecoratorFactorySample("instance method")
printStr() {
console.log(this.str);
}
}
var sampleClass = new SampleClass("こんにちは");
// PropertyDescriptorの出力(デコレータで変更された後の値確認)
console.log(
Object.getOwnPropertyDescriptor(
Object.getPrototypeOf(sampleClass),
"printStr"
)
);
+// setの確認
+sampleClass.printStr = () => {
+ console.log("changed by setter");
+};
+sampleClass.printStr();
結果です。
setterが呼び出され、printStr
メソッドが書き変わったことが分かります。
まとめ
メソッドデコレータ関数の引数をまとめます。
引数名 | インスタンスメンバのメソッドの場合 | staticメソッドの場合 |
---|---|---|
target | クラスのプロトタイプ | クラス(コンストラクタ関数) |
propertyKey | メソッド名 | メソッド名 |
descriptor | PropertyDescriptorのオブジェクト | PropertyDescriptorのオブジェクト |
メソッドデコレータ関数は、PropertyDescripterオブジェクトをreturnすることで、対象メソッドのPropertyDescriptorを上書きすることができました。
PropertyDescriptorオブジェクトの属性をまとめます。
属性名 | 説明 |
---|---|
configurable | プロパティフラグの変更やプロパティの削除を許可するか否か |
enumerable | プロパティを列挙可能にするかどうか |
value | 対象のメソッドそのもの |
writable | プロパティの値書き込み(再代入)を許可するかどうか |
get | 対象メソッドのgetter(value, writableとは同居できない) |
set | 対象メソッドのsetter(value, writableとは同居できない) |
本記事のサンプルコードはGitHubに上げています。
以上。