こんにちは。KOUKIです。
今回は、デコレータについて学びましょう。
環境構築
以下の記事で作成したプロジェクトを使います。
※TypeScriptが動けば問題ありません
デコレータとは
デコレータは、開発者同士で使いやすい機能を提供しあうことに向いています。
百聞は一見に如かずということで、簡単なデコレータを書いてみましょう。
クラスの定義
最初に、デコレータを設定するクラスを定義します。
1 2 3 4 5 6 7 8 9 |
// Book Classを定義 class Book { title = "Harry Potter"; constructor() { console.log("Creating Book Object..."); } } const b = new Book() |
この状態で出力結果を確認すると、以下のようになっています。
1 |
Creating Book Object... |
Bookクラスのコンストラクタは、「インスタンス化した時呼ばれる」ということがわかりますね。
デコレータ用の関数を定義
続いて、デコレータ用の関数を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// デコレータ用のの関数 function Logger(constructor: Function) { console.log("Set Up Logger..."); console.log(constructor); } // Book Classを定義 class Book { title = "Harry Potter"; constructor() { console.log("Creating Book Object..."); } } const b = new Book(); |
デコレータ用の関数の引数に、constructorを設定しています。これは、Bookクラスに対してデコレータを設定する際、Bookクラスのconstructorを引数として受け取る必要があるからです。※クラスに設定する場合
デコレータとして設定
デコレータとして設定するためには、「@」キーワードを用います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function Logger(constructor: Function) { console.log("Set Up Logger..."); console.log(constructor); } @Logger class Book { title = "Harry Potter"; constructor() { console.log("Creating Book Object..."); } } const b = new Book(); |
これだけです。簡単ですよね。
出力結果をみてください。
1 2 3 4 5 6 7 8 |
Set Up Logger... class Book { constructor() { this.title = "Harry Potter"; console.log("Creating Book Object..."); } } Creating Book Object... |
デコレータのコンソール出力が、Bookのconstructorに設定したコンソール出力より、先に出力されています。
つまり、Bookクラスがインスタンス化される前に、デコレータの処理が実行されるのです。
デコレータは、クラスに定義された時に実行されます。
デコレータのカスタマイズ(デコレータ関数)
先ほど作成したデコレータは、コンソールログの出力が呼び出し元から指定できないため、少し不便です。
カスタマイズしてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// string型を受け取れるように変更 function LoggerFn(message: string) { // 匿名関数を作成して、処理を返す return function (constructor: Function) { console.log(message); console.log(constructor); }; } @LoggerFn("Book: Log Set Up") class Book { title = "Harry Potter"; constructor() { console.log("Creating Book Object..."); } } const b = new Book(); |
LoggerからLoggerFnにバージョンアップしました。
ここでは、String型の引数を受け取り、匿名関数を返却する関数に変更しました。
このようにすると、呼び出し元で自由なメッセージをデコレーターに送信することが可能になります。
1 2 3 4 5 6 7 8 |
Book: Log Set UP class Book { constructor() { this.title = "Harry Potter"; console.log("Creating Book Object..."); } } Creating Book Object... |
引数を使わない指定方法
クラスにデコレータを設定する場合はconstructorを受けとる必要がありますが、必ずしもそれを使いたいわけではありません。
その場合は、「_」を指定すれば、エラーなくコンパイルできます。
1 2 3 4 |
function Logger(_: Function) { console.log("Set Up Logger..."); // console.log(constructor); } |
デコレータは結構便利
デコレータは、便利です。以下のようなことも可能です。
1 2 3 4 5 6 7 8 9 10 11 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>TypeScript Test</title> </head> <body> <p id="paragraph"></p> <script src="dist/bundle.js"></script> </body> </html> |
上記には、pタグにid=paragraphを指定しています。このpタグに任意の文字列を追加してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// HTMLタグ情報とIDを受け取る function WithTemplate(message: string, id: string) { return function (_: Function) { const paragraph = document.getElementById(id)!; paragraph.innerHTML = message; }; } @WithTemplate("Hello Decorators Class!", "paragraph") class Book { title = "Harry Potter"; constructor() { console.log("Creating Book Object..."); } } const b = new Book(); |
ブラウザを確認します。

Pタグに任意の文字列が出力されましたね。事前に共通で行っておきたい処理を実装する時に使えそうです。
デコレータの応用
複数のデコレータを設定することも可能ですが、ここで重要なのは、デコレータの実行順序です。
次の通りにデコレータを設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
function LoggerFn() { console.log("Set Up LoggerFn"); return function (_: Function) { console.log("Do LoggerFn"); }; } function WithTemplate(message: string, id: string) { console.log("Set Up WithTemplate"); return function (_: Function) { console.log("Do WithTemplate Func"); const paragraph = document.getElementById(id)!; paragraph.innerHTML = message; }; } @LoggerFn() @WithTemplate("Hello Decorators Class!", "paragraph") class Book { title = "Harry Potter"; constructor() { console.log("Creating Book Object..."); } } const b = new Book(); |
BookクラスにLoggerFnとWithTemplateデコレータをそれぞれ設定しました。
この場合、どのような順序で処理が実行されるのでしょうか。
出力結果を確認しましょう。
1 2 3 4 5 |
Set Up LoggerFn Set Up WithTemplate Do WithTemplate Func Do LoggerFn Creating Book Object... |
「Set Up LoggerFn」と「Set Up WithTemplate」から@Logger -> @WithTemplateの順で処理が実行されているのがわかります。
しかし、そのあとの出力結果である「Do With Template Func」と「Do Logger Fn」から@WithTemplate->@Loggerの順で処理が実行されていることがわかります。
つまり、デコレータは設定順に実行され、デコレータ内の無名関数はその逆順から実行されていくわけですね。

結構複雑ですね。。
デコレータを追加できる場所
デコレータを追加するにはクラスが必要ですが、必ずしもクラスそのものに設定する必要はないです。
例えば、プロパティに設定することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// 第一引数 // インスタンスのプロパティの場合は、そのクラスのプロトタイプが渡される // staticメンバーの場合は、コンストラクタ関数が渡される // 第二引数 // プロパティの名前(title) function Logger(target: any, propertyName: string | Symbol) { console.log("Property Decorators"); console.log(target); console.log(propertyName); } class Book { @Logger title: string; private _price: number; set price(val: number) { if (val > 0) { this._price = val; } else { throw new Error(" No Invalid Price!!!"); } } constructor(t: string, p: number) { this.title = t; this._price = p; } getPriceWithTax(tax: number) { return this._price * (1 + tax); } } const b = new Book("Harry Potter", 3000); |
1 2 3 |
Property Decorators {constructor: ƒ, getPriceWithTax: ƒ}constructor: class BookgetPriceWithTax: ƒ getPriceWithTax(tax)set price: ƒ price(val)__proto__: Object title |
set priceなどのアクセサやメソッドにもデコレータを設定することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
ffunction Logger(target: any, propertyName: string | Symbol) { console.log("Property Decorators"); console.log(target); console.log(propertyName); } function Logger2(target: any, name: string, description: PropertyDescriptor) { console.log("Accessor Decorators"); console.log(target); console.log(name); console.log(description); } function Logger3( target: any, name: string | symbol, descriptor: PropertyDescriptor ) { console.log("Method Decorators"); console.log(target); console.log(name); console.log(descriptor); } class Book { @Logger title: string; private _price: number; @Logger2 set price(val: number) { if (val > 0) { this._price = val; } else { throw new Error(" No Invalid Price!!!"); } } constructor(t: string, p: number) { this.title = t; this._price = p; } @Logger3 getPriceWithTax(tax: number) { return this._price * (1 + tax); } } const b = new Book("Harry Potter", 3000); |
パラメータにもデコレータが追加できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// positionにはパラメータを設定した番号が渡される // 1つ目のパラメータの場合は、0 function Logger4(target: any, name: string | Symbol, position: number) { console.log("Parameter Decorators"); console.log(target); console.log(name); console.log(position); } class Book { @Logger title: string; private _price: number; @Logger2 set price(val: number) { if (val > 0) { this._price = val; } else { throw new Error(" No Invalid Price!!!"); } } constructor(t: string, p: number) { this.title = t; this._price = p; } @Logger3 getPriceWithTax(@Logger4 tax: number) { return this._price * (1 + tax); } } const b = new Book("Harry Potter", 3000); |
デコレータで戻り値を返す
デコレータは、戻り値を返すことが可能です。しかし、デコレータを設定する場所によって実装が少し異なるので、注意が必要です。
書き換えたクラスを返す
少し難しいですが、デコレータを設定したクラスを書き換えるデコレータを実装しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// デコレータを設定したクラスを書き換える // これはクラスに設定するデコレータなので、コンストラクタを引数として受け取っていることが前提 function ChangeCls(message: string) { console.log('Change Class Decorators') // インスタンス後に実行されるため、ジェネリックを用いる // newは受け取ったデータ(ここではクラス)がインスタンス化できることを保証する // titleはクラスがtitleプロパティを持っていることを明示的に指定している。 // これによりthis.titleのようにプロパティにアクセスできる return function<T extends { new (...args: any[]): { title: string } }>( originalConstructor: T) { console.log('Decorator Function') // 書き換えを始めるため、オリジナルのコンストラクタを継承したクラスを戻り値とする // クラスはコンストラクタのシンタックスシュガー(別の構文に書き直したもの)のため、 // クラスを返すことはコンストラクタを返すことと同義 return class extends originalConstructor { // コンストラクタを定義 // 通常は...argsと書くが今回はこの引数を使わない場合は、_にてスキップできる // ...は任意の数の引数を受け取ることを可能にする constructor(...args: any[]) { console.log('Extends constructor') // 親クラスのコンストラクタを呼びだす super(args[0]) const hookEl = document.getElementById('paragraph') if (hookEl) { // 戻り値に title: stringを設定しているので、TypeScriptは渡したクラスが、 // titleプロパティを持っていることを認識する hookEl.innerText = this.title + ' ' + message } } } } } @ChangeCls("Wonderful!!!") class Book { title: string; constructor(t: string) { console.log('Original constructor') this.title = t; } } const b = new Book("Harry Potter"); |
理解に必要なことはコメントに書きましたが、難しいですよね。
ChangeClsは、デコレータ先のコンストラクタを受け取れるので、任意の処理をクラスとして再定義し、新しいクラスを戻り値として返却しています。
出力結果を確認しましょう。
1 2 3 4 |
Change Class Decorators Decorator Function Extends constructor Original constructor |
注目していただきたいのが、処理を実行するタイミングです。
以下の処理をコメントアウトしてから、もう一度出力結果を確認します。
1 |
// const b = new Book("Harry Potter"); |
1 2 |
Change Class Decorators Decorator Function |
「**** constructor」の出力が消えています。デコレータはインスタンス化する前にクラスに定義された時点で処理が実行されますが、戻り値として返却されたクラスは「インスタンス化された時点」で実行されるということです。
その他の値を返す
プロパティディスクリプタを持つデコレータは、値を返すことが可能です。
プロパティディスクリプタは、アクセサ(set ***, get ***)やメソッドにデコレータを付与したとき、3つ目の引数として受け取れるものです。
サンプルを提示します。
1 2 3 4 5 6 7 8 9 10 11 12 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>TypeScript Test</title> </head> <body> <p id="paragraph"></p> <button>Click Me</button> <script src="dist/bundle.js"></script> </body> </html> |
このボタンを押下した時、特定の処理が実行されるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Button { message = 'Clicked!' showMessage() { alert(this.message) } } // Buttonクラスをインスタンス化 const bc = new Button() const btn = document.querySelector('button')! btn.addEventListener('click', bc.showMessage) |
Buttonクラスを定義しました。画面上のボタンが押下された時、showMessageの処理が実行されます。
showMessageでは、alert関数にてメッセージを表示させようとしていますが、「undefined」になってしまいます。

本当なら「Clicked」が表示されて欲しいところですが、addEventListnerにメソッドそのものを渡しているので、undefinedになってしまっています。なぜなら、showMessage内に定義されている「this」はButtonクラスを指すのではなく、別のものを指しているからです。
別のものとは、HTML内に設置したButtonタグです。addEventListnerのようなコールバック関数は、処理が実行されたとき、その対象となったものを関数にバインドするためです。
これは、次の通りに修正すると正しく動作します。
1 2 |
// bindにてButtonのインスタンスを渡す btn.addEventListener('click', bc.showMessage.bind(bc)) |

これはこれで簡単ですが、せっかくデコレータを勉強しているので、メソッドのデコレータにてこの問題を回避しましょう。
やりたいこととしては、showMessageがどこで呼び出されたとしても、Buttonクラスを参照するようにしたいです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// 自動的にバインドするデコレータを作成する function Autobind(_: any, _2: string, descriptor: PropertyDescriptor ) { // 対象のメソッドが呼ばれた時、常にそのメソッドが所属しているオブジェクトが参照されるようにする // valueから元のメソッドを取り出す const originalMethod = descriptor.value // 書き換え後のディスクリプタを定義 const adjDescriptor: PropertyDescriptor = { // プロパティを設定する // プロパティの意味:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty configurable: true, enumerable: false, get() { // 利用者がプロパティにアクセスする前に、何らかの処理を実行する // プロパティとは、descriptor.valueでありここではshowMessage // つまりshowMessageが実行される前に実行される // thisは、getメソッドを実行するオブジェクトを参照する // このような実装をすることで常にButtonクラスが参照される const boundFn = originalMethod.bind(this) return boundFn } } return adjDescriptor } class Button { message = 'Clicked!' @Autobind showMessage() { alert(this.message) } } // Buttonクラスをインスタンス化 const bc = new Button() const btn = document.querySelector('button')! btn.addEventListener('click', bc.showMessage) // bindを外す |
Autobindデコレータを設定しました。意味は、コメントに書いてある通りです。
動作確認をしてみましょう。

OKですね。
デコレータとバリデーション
最後に、デコレータを使ったバリデーション処理をみていきましょう。
バリデーションは、ざっくりいうと入力チェック機能です。
例えば、以下のクラスを例に考えてみましょう。
1 2 3 4 5 6 7 8 9 |
class Post { title: string like: number constructor(t: string, l: number){ this.title = t this.like = l } } |
このPostクラスには、titleとlikeという二つのプロパティを持っています。
このプロパティに設定する値が「正しいもの、想定されたもの」なのかをバリデーションチェックしたいと思います。
入力項目も作成しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>TypeScript Test</title> </head> <body> <p id="paragraph"></p> <form> <input type="text" placeholder="title" id="title"> <input type="text" placeholder="like" id="like"> <button type="submit">Post</button> </form> <script src="dist/bundle.js"></script> </body> </html> |

続いて、このformから値を取得する処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 |
const postForm = document.querySelector('form')! postForm.addEventListener('submit', event => { event.preventDefault() const titleEl = document.getElementById('title') as HTMLInputElement const likeEl = document.getElementById('like') as HTMLInputElement const title = titleEl.value const like = Number(likeEl.value) const createdPost = new Post(title, like) console.log(createdPost) }) |
出力結果を確認します。

項目に入力した値が、コンソールに出力されていますね。
titleにはstring型、likeにはnumber型の値が入る想定です。
それでは、次にtitleにnumber型、likeにstring型の値を入力してみましょう。

出力結果を確認すると。likeにはNan, titleには”10″という結果が出力されました。これは、、想定した挙動ではありません。
対処方法としては、バリデーションチェックで、想定していない型の入力に対して何らかのアクションが必要です。
バリデーションチェックを実装するには、index型を使用します。index型については、以下の記事で解説しています。
それでは、実装してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
// 1. バリデーション用のインターフェースを定義 interface ValidatorConfig { // index型を定義 // propには、クラス名を指定 // 値はオブジェクト [prop: string]: { // index型を定義 // 型はstringで、string型の配列を格納する [validatableProp: string]: string[] // ['required], 'positive'] } } // 2. ValidatorConfigを使ってオブジェクトを定義する // 空のオブジェクトだが、バリデーションが実行されたら値が登録される const registeredValidators: ValidatorConfig = {} // 3. 必須チェックのデコレータ // ValidtorConfigのindex型通りに定義する function Required(target: any, propName: string) { // targetには、プロトタイプかコンストラクタが入る // そして、nameプロパティにアクセスするとclass名が取得できる console.log(target) registeredValidators[target.constructor.name] = { // クラスのバリデーターの設定を上書きしないようにコピーするようにする ...registeredValidators[target.constructor.name], // propNameは、プロパティ名が入る [propName]: [ // バリデータの値も複数入るようにする ...(registeredValidators[target.constructor.name]?.[propName] ?? []), 'required' ] } } // 3. 数値チェックのデコレータ function PositiveNumber(target: any, propName: string) { registeredValidators[target.constructor.name] = { // クラスのバリデーターの設定を上書きしないようにコピーするようにする ...registeredValidators[target.constructor.name], [propName]: [ // バリデータの値も複数入るようにする ...(registeredValidators[target.constructor.name]?.[propName] ?? []), 'positive' ] } } // 4. バリデーションチェック関数 function validate(obj: any) { // 最初に引数として受け取ったオブジェクトがどのクラスのものなのか知る必要がある // チェックしたバリデーション情報を取得することが目的 const objValidatorConfig = registeredValidators[obj.constructor.name] if (!objValidatorConfig) { // 検証するものがない場合は、trueを返す return true } let isValid = true // バリデーションとして登録されている情報をループで取り出す // 最初はパラメータ for (const param in objValidatorConfig) { // 次はバリデーションチェック項目(required, positive) for (const validator of objValidatorConfig[param]) { switch(validator) { case 'required': // requiredでは、引数のobjのパラメータがからでない場合はtrueを返す // !!をつけるとtruthyのあたいであれば、booleanに変換される // !!1 -> true // !!0 -> false isValid = isValid && !!obj[param] break case 'positive': isValid = isValid && obj[param] > 0 break } } } return isValid } class Post { // 5. デコレータを設定する @Required title: string @PositiveNumber like: number constructor(t: string, l: number){ this.title = t this.like = l } } const postForm = document.querySelector('form')! postForm.addEventListener('submit', event => { event.preventDefault() const titleEl = document.getElementById('title') as HTMLInputElement const likeEl = document.getElementById('like') as HTMLInputElement const title = titleEl.value const like = Number(likeEl.value) const createdPost = new Post(title, like) // 6. バリデーションチェックをする if (!validate(createdPost)) { alert('Invalid Parameter') return } console.log(createdPost) }) |
具体的な説明については、コメントに書きました。
バリデーションとしてチェックしたのは、必須チェックとポジティブナンバー(正の値)チェックです。
動作確認をしてみましょう。


補足ですが、上記コードにある「??」は、Nullish Coalescingという構文です。
??の左側の値がnulまたはundefinedの場合は、??の右型の値を利用します。
三項演算子と似ていますね。
1 2 3 4 5 6 7 8 9 |
// 三項演算子 const yummy: null | string = null; const food = yumm ? yummy : "bow" console.log(food) // bow // Nullish Coalescing const yummy: null | string = null; const food = yumm ?? : "bow" // 三項演算子より短い console.log(food) // bow |
おわりに
TypeScriptのデコレータについては以上です。
デコレータは本当に便利なので、使いこなしていきたいですね^^
それでは、また!
最近のコメント