ECMAScriptに対応した新たなデコレータ
本連載は、TypeScriptのバージョン3から5.2までのアップデート内容を、テーマごとにバージョン横断で紹介する連載です。今回でこの連載も一区切りとなり、5.2までの変更点をまとめる最後となります。その最後の回として、デコレータを紹介します。
デコレータとは
デコレータというのは、リスト1の(1)のように、クラス中に記述された@から始まるコードのことです。
class SelfIntro { @logging // (1) show() { console.log("こんにちは!"); } }
このデコレータという仕組みは、TypeScriptのバージョン1.5という初期の頃から存在しています。これは、ES7に向けてECMAScriptで提案されていたものを先行でTypeScriptに導入された仕組みです。このECMAScriptで提案されていたデコレータが、ようやくステージ3になったのを機に、そのECMAScriptの仕様に従った新たな仕組みのデコレータとして、バージョン5.0でTypeScriptに組み込まれました。
デコレータの働き
このデコレータの働きを端的にいうと、クラスのメンバに対して処理を付与できる仕組みです。例えば、リスト1の例でいうと、show()メソッドの本来の処理は、コンソールに「こんにちは!」と表示させることです。この本来の処理とは別に、何か処理を付与したい場合に、例えば、logging()という関数を定義しておき、(1)の@loggingのように、その関数名に@をつけた記述を行います。この仕組みにより、show()メソッドが実行されるたびに、logging()関数内の処理も実行されるようになります。
デコレータの簡易的な作り方
概論はここまでにしておき、実際にデコレータの作り方を紹介していきます。デコレータはメソッドはもちろん、任意のクラスメンバ、場合によってはクラス定義そのものに対しても付与できますが、その定義方法は対象によってほとんど違いはありません。そのため、リスト1に記述したメソッドに付与するlogging()関数を題材に、デコレータの作り方の基礎を紹介していきます。
デコレータ関数の基本形
loggingデコレータ関数の一番簡易なコード例は、リスト2のようなものとなります。
function logging(originalMethod: any, context: any) { // (1) function replacementMethod(this: any, ...args: any[]) { // (2) console.log("メソッドが実行されました。"); // (3) originalMethod(); // (4) } return replacementMethod; // (5) }
デコレータ関数の基本形は、(1)のように関数名をデコレータ名そのものとし、その中にさらに関数を定義し、その関数をリターンするコードとします。リスト2では、(2)のreplacementMethod()関数が該当し、(5)のようにリターンしています。そして、このリターンする関数の中に、デコレータとして付与したい処理を記述します。
その際、関数内で本来の処理、例えば、リスト1ではloggingデコレータが付与されたshow()メソッドそのものの処理を呼び出す必要があります。リスト2では、(3)の「メソッドが実行されました。」とコンソール表示するコードがデコレータとして付与された処理であり、(4)のコードがデコレータが付与されたメソッド本来の処理の呼び出しコードです。このデコレータが付与されたメソッド本来の処理は、デコレータ関数本体の第1引数として渡されます。リスト2の(1)ではoriginalMethodとしていますので、これを(4)のように関数として実行することで、デコレータが付与されたメソッド本来の処理が実行されます。
結果、リスト1のSelfIntroをnewしてshow()メソッドを実行した場合、コンソールには次のように表示されます。
メソッドが実行されました。 こんにちは!
これは、リスト2の(3)と(4)のように、「デコレータの処理→デコレータを付与したメソッド本来の処理」の順序でコードを記述したからです。この順序は、もちろん任意であり、リスト2の(3)と(4)のコードを入れ替えると、次のように実行結果も入れ替わります。
こんにちは! メソッドが実行されました。
replacementMethod()関数定義の注意点
なお、リスト2では、可読性を重視し、replacementMethod()関数を定義した上でリターンしていますが、次のように無名関数を利用してもかまいません。
function logging(originalMethod: any, context: any) { return function(this: any, ...args: any[]) { : } }
ただし、次のようにアロー関数は利用できないので、注意しましょう。というのは、後述するように、デコレータではthisを利用するのですが、このthisが実行時に決まらなければならないからです(アロー関数では定義時に決定します)。実際、TypeScriptでは、以下のコードはエラーとなります。
function logging(originalMethod: any, context: any) { return (this: any, ...args: any[]) => { : } }
メソッドの引数
リスト1のshow()には引数は定義されていませんでした。これが、もしshow(name: String)のように引数がある場合は、リスト2の(4)のoriginalMethodを実行する際に引数を渡す必要があります。そのための引数が、replacementMethod()関数の第2引数であり、デコレータを付与するメソッドが実行される際に渡された引数データがそのまま渡される仕組みとなっています。そこでこれを利用して、リスト3のようなコードを記述します。
function logging(originalMethod: any, context: any) { function replacementMethod(this: any, ...args: any[]) { console.log("メソッドが実行されました。"); originalMethod(...args); } return replacementMethod; }
単に、replacementMethod()の第2引数であるargsを、originalMethod()の実行時に渡すだけです。このargsは、元々のメソッド引数の個数が何個か分かりませんので、可変長引数となっています。これをもとのメソッドに渡すために、スプレッド演算子を利用している点には注意してください。
メソッドの戻り値
次に、デコレータを付与するメソッドに戻り値がある場合のコードサンプルを紹介するとします(リスト4)。
なんのことはありません。originalMethodを実行した際に、デコレータを付与するメソッドの戻り値がそのまま返ってくるので、(1)のようにそれを受け取り(2)のようにreplacementMethod()関数の戻り値とするだけです。
function logging(originalMethod: any, context: any) { function replacementMethod(this: any, ...args: any[]) { console.log("メソッドが実行されました。"); const originalReturn = originalMethod(...args); // (1) return originalReturn; // (2) } return replacementMethod; }
もちろん、(1)と(2)の間に、コンソール出力などの処理を挟まないのならば、(1)と(2)は次のように1行で記述してもかまいません。
return originalMethod(...args);