Tweet
Logo
    Typescript の Decorator
    Typescript の Decorator

    Typescript の Decorator

    • はじめに
    • Decorator とは
    • 実行してみる
    • 改めて Decorator 関数が何を行なっているかを理解する
    • Decorator Factory
    • Decorator の種類
    • Class Decorators
    • Method Decorators
    • Accessor Decorators
    • Property Decorators
    • Parameter Decorators
    • おわりに

    はじめに

    この記事では Typescript の Decorator の挙動について基礎的な部分を解説します

    動作の検証は以下のバージョンのツールで行ないました

    $ tsc --version
    Version 4.3.2
    
    $ ts-node --version
    v10.0.0

    Decorator とは

    Typescript の Decorator とは Class 内のメソッドやプロパティを修正したり変更したりできる関数の事です

    公式にはここに説明があります

    Documentation - Decorators

    TypeScript Decorators overview

    www.typescriptlang.org

    Documentation - Decorators

    公式の説明によると、(2021年6月現在) Decorator は JavaScript の stage 2 proposal との事で、今後仕様に変更が入る可能性があります。またそのため、以下のように tsconfig で experimentalDecorators flag を true にしないと使えません

    {
      "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
      }
    }

    実行してみる

    Decorator は概念だけだとかなり解りづらいと思うので実行してみるのが理解の近道だとおもいます

    簡単なコードを走らせて挙動を確認してみます

    test.ts

    上記コードを適当な名前で保存して (ここでは test.ts) tsc-node を使って実行してみます

    $ ts-node test.ts 
    target { throwError: [Function (anonymous)] }
    key throwError
    desc {
      value: [Function (anonymous)],
      writable: true,
      enumerable: true,
      configurable: true
    }
    error catched

    どうやら TestClass の throwError メソッドが実行されて、その際に Decorator 関数内部で try catch に引っかかりコンソールログが出力されているようです

    以下のコマンドで transpile して詳しくみてみます

    $ tsc test.ts 
    test.js

    Decorator 関数の第一引数である target には TestClass class の prototype が入り、第二引数にはデコレートした関数名が入り呼び出されています。またコードを見ると Decorator はクラスが定義された瞬間に実行されています。Decorator はクラスインスタンスが実体化されるタイミングや、デコレートされたメソッドなどが実行されるタイミングで呼ばれるわけでは無い事がわかります

    Decorator 関数の第三引数に入る desc は __decorate 関数の getOwnPropertyDescriptor で取得された PropertyDescriptor が入ります

    desc = Object.getOwnPropertyDescriptor(target, key)

    PropertyDescriptor は ES5 で定義された JS object の property を記述する構造体です

    console log された結果を見ると以下のようなものになっており、value, writable, enumerable, configurable という 4 つの property を持つ事がわかります

    desc {
      value: [Function (anonymous)],
      writable: true,
      enumerable: true,
      configurable: true
    }

    ここで、 value には decorate した関数そのものが入っています

    getOwnPropertyDescriptor の挙動に関してはこちらで確認できます

    developer.mozilla.org

    developer.mozilla.org

    改めて Decorator 関数が何を行なっているかを理解する

    上記を踏まえて再度 Decorator 関数を見てみると、何を行なっているかが一目瞭然です

    function logError(target: any, key: string, desc: PropertyDescriptor): void {
      console.log("target", target)
      console.log("key", key)
      console.log("desc", desc)
    
      const method = desc.value
    
      desc.value = function() {
        try {
          method()
        } catch (e) {
          console.log("error ctched")
        }
      }
    
    }

    実際に行なっている事は非常にシンプルで主に以下の 2 つです

    1. この関数はクラスの定義コードが走った際に 1 度だけ実行され
    2. 実行時に PropertyDescriptor を受け取り、関数を try catch で wrap した関数を作り、それを property にセットし直す

    これだけで元々の関数を変更せずに Decorator 関数がセットされた関数が呼び出された際に exception が発生すると catch して error を console に出してくれるようにできます

    Decorator Factory

    上の例では logError Decorator が catch した際に console.log するメッセージは定数でした

    例えばメソッドによってメッセージを変更する、などを行いたい場合には Decorator Factory を使う事で可能です

    function color(value: string) {
      // this is the decorator factory, it sets up
      // the returned decorator function
      return function (target) {
        // this is the decorator
        // do something with 'target' and 'value'...
      };
    }
    公式から引用

    例えば logError の例だと、以下のように wrap する事で Decorator 関数に設定値を受け取る事ができます (ここで受け取る値はクラスの定義実行時に 1 度だけで、関数の実行時に値を受け取るわけでは無い事に注意する必要があります)

    function logError(message: string) {
    
    	return function(target: any, key: string, desc: PropertyDescriptor): void {
    	  console.log("target", target)
    	  console.log("key", key)
    	  console.log("desc", desc)
    
    	  const method = desc.value
    
    	  desc.value = function() {
    	    try {
    	      method()
    	    } catch (e) {
    	      console.log(message)
    	    }
    	  }
    
    	}
    }
    
    Decorator Factory を使った logError

    これを使う場合には以下のようになります

    class TestClass {
    
      @logError("throwError でエラーになったよ")
      throwError(): void {
          throw new Error()
      }
    
      @logError("process でエラーになったよ. やばい!!")
      process(): void {
          console.log('普通の処理')
      }
    
    }

    Decorator の種類

    ここまで Method Decorators をベースに挙動を説明してきましたが、Decorator には色々な種類があるので公式のコードを引用しつつ紹介します

    Class Decorators

    Class Decorators はクラスのコンストラクタに適用され、クラス定義への修正などを行います

    Class Decorator は引数として constructor を受け取ります

    公式から引用

    Class Decorator で property を追加したとしても TypeScript の type system には認識してくれないので、 ts-node などで直接実行しようとするとエラーになるので注意が必要です

    Method Decorators

    Method Decorators は挙動の概要で説明したものです

    引数として target key descriptor を受け取ります

    class Greeter {
      greeting: string;
      constructor(message: string) {
        this.greeting = message;
      }
    
      @enumerable(false)
      greet() {
        return "Hello, " + this.greeting;
      }
    }
    
    function enumerable(value: boolean) {
      return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.enumerable = value;
      };
    }
    公式から引用

    descriptor の property を使ってメソッドの挙動を変更する事が可能です

    Accessor Decorators

    Method Decorator と同じ挙動をします

    公式から引用

    Property Decorators

    property decorator は property に対して宣言します

    Decorator 関数が受け取る引数は以下の 2 つになります

    • class の prototype (もしくは class の constructor)
    • member の名前

    以下は

    class Greeter {
    
      @logPropsCalled
      greeting: string;
    
      constructor(message: string) {
        this.greeting = message;
      }
    
    }
    
    function logPropsCalled(target: any, propertyKey: string): void {
      console.log(`${propertyKey} is called`)
    }
    
    console.log(new Greeter('message').greeting)
    公式から引用をシンプルな形に修正

    これを実行すると以下のようになります

    $ ts-node greeter.ts 
    greeting is called
    message

    Parameter Decorators

    パラメーター宣言の直前で宣言します

    下記公式の例では、Method Decorator と組み合わせて、引数のチェックを行なっています

    具体的には required Decorator で必須となる parameter を宣言し、validate で必須となる引数が undefined じゃない事をチェックしています

    公式から引用

    おわりに

    Typescript の Decorator の基本について解説しました

    Decorator は Experimental 機能でありながら数多くの Framework などでも採用されており、しっかりと理解しておいて損の無い機能だとおもいます

    © 2025 DROBE All rights reserved.
    class TestClass {
    
      @logError
      throwError(): void {
          throw new Error()
      }
    
    }
    
    function logError(target: any, key: string, desc: PropertyDescriptor): void {
      console.log("target", target)
      console.log("key", key)
      console.log("desc", desc)
    
      const method = desc.value
    
      desc.value = function() {
        try {
          method()
        } catch (e) {
          console.log("error catched")
        }
      }
    
    }
    
    new TestClass().throwError()
    var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
        var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
        if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
        else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
        return c > 3 && r && Object.defineProperty(target, key, r), r;
    };
    var TestClass = /** @class */ (function () {
        function TestClass() {
        }
        TestClass.prototype.throwError = function () {
            throw new Error();
        };
        __decorate([
            logError
        ], TestClass.prototype, "throwError");
        return TestClass;
    }());
    function logError(target, key, desc) {
        console.log("target", target);
        console.log("key", key);
        console.log("desc", desc);
        var method = desc.value;
        desc.value = function () {
            try {
                method();
            }
            catch (e) {
                console.log("error catched");
            }
        };
    }
    new TestClass().throwError();
    function reportableClassDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
      return class extends constructor {
        reportingURL = "http://www...";
      };
    }
    
    @reportableClassDecorator
    class BugReport {
      type = "report";
      title: string;
    
      constructor(t: string) {
        this.title = t;
      }
    }
    
    const bug = new BugReport("Needs dark mode");
    console.log(bug.title); // Prints "Needs dark mode"
    console.log(bug.type); // Prints "report"
    
    // // Note that the decorator _does not_ change the TypeScript type
    // // and so the new property `reportingURL` is not known
    // // to the type system:
    console.log(bug.reportingURL);
    class Point {
      private _x: number;
      private _y: number;
      constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
      }
    
      @configurable(false)
      get x() {
        return this._x;
      }
    
      @configurable(false)
      get y() {
        return this._y;
      }
    }
    
    function configurable(value: boolean) {
      return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
      };
    }
    class BugReport {
      type = "report";
      title: string;
    
      constructor(t: string) {
        this.title = t;
      }
    
      @validate
      print(@required verbose: boolean) {
        if (verbose) {
          return `type: ${this.type}\ntitle: ${this.title}`;
        } else {
         return this.title; 
        }
      }
    }
    
    import "reflect-metadata";
    const requiredMetadataKey = Symbol("required");
    
    function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
      let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
      existingRequiredParameters.push(parameterIndex);
      Reflect.defineMetadata( requiredMetadataKey, existingRequiredParameters, target, propertyKey);
    }
    
    function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
      let method = descriptor.value!;
    
      descriptor.value = function () {
        let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
        if (requiredParameters) {
          for (let parameterIndex of requiredParameters) {
            if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
              throw new Error("Missing required argument.");
            }
          }
        }
        return method.apply(this, arguments);
      };
    }