TypeScriptでデコレータを学ぼう!

こんにちは。KOUKIです。

今回は、デコレータについて学びましょう。

環境構築

以下の記事で作成したプロジェクトを使います。
※TypeScriptが動けば問題ありません

デコレータとは

デコレータは、開発者同士で使いやすい機能を提供しあうことに向いています。

百聞は一見に如かずということで、簡単なデコレータを書いてみましょう。

クラスの定義

最初に、デコレータを設定するクラスを定義します。

この状態で出力結果を確認すると、以下のようになっています。

Bookクラスのコンストラクタは、「インスタンス化した時呼ばれる」ということがわかりますね。

デコレータ用の関数を定義

続いて、デコレータ用の関数を定義します。

デコレータ用の関数の引数に、constructorを設定しています。これは、Bookクラスに対してデコレータを設定する際、Bookクラスのconstructorを引数として受け取る必要があるからです。※クラスに設定する場合

デコレータとして設定

デコレータとして設定するためには、「@」キーワードを用います。

これだけです。簡単ですよね。

出力結果をみてください。

デコレータのコンソール出力が、Bookのconstructorに設定したコンソール出力より、先に出力されています。

つまり、Bookクラスがインスタンス化される前に、デコレータの処理が実行されるのです

デコレータは、クラスに定義された時に実行されます。

デコレータのカスタマイズ(デコレータ関数)

先ほど作成したデコレータは、コンソールログの出力が呼び出し元から指定できないため、少し不便です。

カスタマイズしてみましょう。

LoggerからLoggerFnにバージョンアップしました。

ここでは、String型の引数を受け取り、匿名関数を返却する関数に変更しました。

このようにすると、呼び出し元で自由なメッセージをデコレーターに送信することが可能になります。

引数を使わない指定方法

クラスにデコレータを設定する場合はconstructorを受けとる必要がありますが、必ずしもそれを使いたいわけではありません。

その場合は、「_」を指定すれば、エラーなくコンパイルできます。

デコレータは結構便利

デコレータは、便利です。以下のようなことも可能です。

上記には、pタグにid=paragraphを指定しています。このpタグに任意の文字列を追加してみましょう。

ブラウザを確認します。

Pタグに任意の文字列が出力されましたね。事前に共通で行っておきたい処理を実装する時に使えそうです。

デコレータの応用

複数のデコレータを設定することも可能ですが、ここで重要なのは、デコレータの実行順序です。

次の通りにデコレータを設定します。

BookクラスにLoggerFnとWithTemplateデコレータをそれぞれ設定しました。

この場合、どのような順序で処理が実行されるのでしょうか。

出力結果を確認しましょう。

「Set Up LoggerFn」と「Set Up WithTemplate」から@Logger -> @WithTemplateの順で処理が実行されているのがわかります。

しかし、そのあとの出力結果である「Do With Template Func」と「Do Logger Fn」から@WithTemplate->@Loggerの順で処理が実行されていることがわかります。

つまり、デコレータは設定順に実行され、デコレータ内の無名関数はその逆順から実行されていくわけですね。

KOUKI
KOUKI

結構複雑ですね。。

デコレータを追加できる場所

デコレータを追加するにはクラスが必要ですが、必ずしもクラスそのものに設定する必要はないです。

例えば、プロパティに設定することができます。

set priceなどのアクセサやメソッドにもデコレータを設定することができます。

パラメータにもデコレータが追加できます。

デコレータで戻り値を返す

デコレータは、戻り値を返すことが可能です。しかし、デコレータを設定する場所によって実装が少し異なるので、注意が必要です。

書き換えたクラスを返す

少し難しいですが、デコレータを設定したクラスを書き換えるデコレータを実装しました。

理解に必要なことはコメントに書きましたが、難しいですよね。

ChangeClsは、デコレータ先のコンストラクタを受け取れるので、任意の処理をクラスとして再定義し、新しいクラスを戻り値として返却しています。

出力結果を確認しましょう。

注目していただきたいのが、処理を実行するタイミングです。

以下の処理をコメントアウトしてから、もう一度出力結果を確認します。

「**** constructor」の出力が消えています。デコレータはインスタンス化する前にクラスに定義された時点で処理が実行されますが、戻り値として返却されたクラスは「インスタンス化された時点」で実行されるということです。

その他の値を返す

プロパティディスクリプタを持つデコレータは、値を返すことが可能です。

プロパティディスクリプタは、アクセサ(set ***, get ***)やメソッドにデコレータを付与したとき、3つ目の引数として受け取れるものです。

サンプルを提示します。

このボタンを押下した時、特定の処理が実行されるようにします。

Buttonクラスを定義しました。画面上のボタンが押下された時、showMessageの処理が実行されます。

showMessageでは、alert関数にてメッセージを表示させようとしていますが、「undefined」になってしまいます。

本当なら「Clicked」が表示されて欲しいところですが、addEventListnerにメソッドそのものを渡しているので、undefinedになってしまっています。なぜなら、showMessage内に定義されている「this」はButtonクラスを指すのではなく、別のものを指しているからです。

別のものとは、HTML内に設置したButtonタグです。addEventListnerのようなコールバック関数は、処理が実行されたとき、その対象となったものを関数にバインドするためです。

これは、次の通りに修正すると正しく動作します。

これはこれで簡単ですが、せっかくデコレータを勉強しているので、メソッドのデコレータにてこの問題を回避しましょう。

やりたいこととしては、showMessageがどこで呼び出されたとしても、Buttonクラスを参照するようにしたいです。

Autobindデコレータを設定しました。意味は、コメントに書いてある通りです。

動作確認をしてみましょう。

OKですね。

デコレータとバリデーション

最後に、デコレータを使ったバリデーション処理をみていきましょう。

バリデーションは、ざっくりいうと入力チェック機能です。

例えば、以下のクラスを例に考えてみましょう。

このPostクラスには、titleとlikeという二つのプロパティを持っています。

このプロパティに設定する値が「正しいもの、想定されたもの」なのかをバリデーションチェックしたいと思います。

入力項目も作成しましょう。

続いて、このformから値を取得する処理を実装します。

出力結果を確認します。

項目に入力した値が、コンソールに出力されていますね。

titleにはstring型、likeにはnumber型の値が入る想定です。

それでは、次にtitleにnumber型、likeにstring型の値を入力してみましょう。

出力結果を確認すると。likeにはNan, titleには”10″という結果が出力されました。これは、、想定した挙動ではありません。

対処方法としては、バリデーションチェックで、想定していない型の入力に対して何らかのアクションが必要です。

バリデーションチェックを実装するには、index型を使用します。index型については、以下の記事で解説しています。

それでは、実装してみましょう。

具体的な説明については、コメントに書きました。

バリデーションとしてチェックしたのは、必須チェックとポジティブナンバー(正の値)チェックです。

動作確認をしてみましょう。

正しい挙動
誤った挙動

補足ですが、上記コードにある「??」は、Nullish Coalescingという構文です。

??の左側の値がnulまたはundefinedの場合は、??の右型の値を利用します。

三項演算子と似ていますね。

おわりに

TypeScriptのデコレータについては以上です。

デコレータは本当に便利なので、使いこなしていきたいですね^^

それでは、また!

関連記事

参考書籍