LoginSignup
2
0

More than 1 year has passed since last update.

TypeScriptのメソッドデコレータを試す

Posted at

筆者がTypeScriptを勉強する中でデコレータについて学んだのですが、どうにもデコレータの記法や動作が記憶に定着しないので、備忘録も兼ねて記事化します。
(勉強中につき、内容に誤りがある可能性があります。)

TypeScriptのデコレータには以下の5種類があります。

  • クラスデコレータ
  • メソッドデコレータ
  • プロパティデコレータ
  • アクセサデコレータ
  • パラメータデコレータ

が、本記事では、

  • メソッドデコレータ

について動作を確認します。

デコレータに関する公式のドキュメントは以下になります。

サンプルプロジェクトの作成

デコレータの動作を確認するためのサンプルプロジェクトを作成します。
とりあえずTypeScriptが動くまでのところは、筆者が以前投稿した記事に沿って作成します。

WebページとコンソールそれぞれにHelloと表示されるだけの状態になります。
typescript.png

メソッドデコレータを試す

まず、デコレータを使うには、tsconfig.jsonを編集する必要があります。experimentalDecoratorsのコメントアウトを外します。

./tsconfig.json
{
  "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を編集します。

./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

スクリーンショット 2022-09-22 19.46.56.png
デコレータファクトリ
→デコレータ関数
→コンストラクタ
→対象メソッド

の順で実行されているように見えます。

書き方・使い方

メソッドのデコレータファクトリとデコレータですが、以下のように定義します。

// デコレータファクトリ(引数は何でも良い)
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);
  };
}

インスタンスメンバのメソッドの場合

コンソール出力の結果は以下です。
スクリーンショット 2022-09-23 11.25.01.png
targetには、クラスのプロトタイプが入っています。

なので、例えば以下のようにメソッドデコレータ関数の中で、クラスのインスタンス化もできます(これが何に使えるかはさておき…)。

function methodDecoratorFactorySample(str: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log(target);
+   var tmp = new target.constructor("こんばんは");
+   tmp.printStr();
  };
}

スクリーンショット 2022-09-23 11.47.47.png

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();
  };
}

この時のコンソール出力が以下です。
スクリーンショット 2022-09-23 19.20.31.png
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);
  };
}

インスタンスメンバのメソッドの場合

propertyKeyには、メソッド名が入っています。
スクリーンショット 2022-09-24 14.34.58.png

staticメソッドの場合

こちらでも同様に、propertyKeyには、メソッド名が入っています。
スクリーンショット 2022-09-24 14.36.05.png

descriptor

descriptorをコンソールに出力します。

function methodDecoratorFactorySample(str: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log(descriptor);
  };
}

インスタンスメンバのメソッドの場合

descriptorには、PropertyDescriptorのオブジェクトが入っています。
スクリーンショット 2022-09-24 14.39.44.png

staticメソッドの場合

descriptorには、PropertyDescriptorのオブジェクトが入っています。オブジェクトの内容は、インスタンスメンバのメソッドの場合と同じです。
スクリーンショット 2022-09-24 14.42.22.png

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メソッドの削除には成功し、メソッドが実行できなくなっていることが分かります。
スクリーンショット 2022-09-24 15.44.50.png

  • 次に、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の場合は、メソッドを削除できないことが確認できました。
スクリーンショット 2022-09-24 15.47.56.png

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のパターン。

printStrメソッドは列挙されていません。
スクリーンショット 2022-09-25 18.38.14.png

  • enumerable: trueのパターン。

デコレータ関数内で、enumerabletrueに設定します。

// デコレータファクトリ
function methodDecoratorFactorySample(str: string) {
  // デコレータ関数を返す
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    // -- descriptorの確認 --
    console.log(descriptor);
+   // - enumerableの確認
+   descriptor.enumerable = true;
  };
}

printStrメソッドが列挙されました。
スクリーンショット 2022-09-25 18.38.36.png

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となっています。
また、メソッドの書き換えができていることも確認できました。
スクリーンショット 2022-09-25 18.57.52.png

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();

問題なく、メソッドの再代入が行われています。
スクリーンショット 2022-09-25 22.13.08.png

  • writable:falseのパターン

デコレータ関数内で、writablefalseを設定します。

// デコレータファクトリ
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となりました。
スクリーンショット 2022-09-25 22.11.09.png

get

対象メソッドのgetterです。PropertyDescriptorオブジェクトの中で、valueおよびwritable属性とは同居できません。

以下、実際に試してみます。
これまでは引数のdescriptorを直接変更していましたが、getvaluewritable属性と同居できないので、新しく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が呼ばれたことが分かります。
スクリーンショット 2022-09-26 0.06.19.png

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メソッドが書き変わったことが分かります。
スクリーンショット 2022-09-26 0.37.49.png

まとめ

メソッドデコレータ関数の引数をまとめます。

引数名 インスタンスメンバのメソッドの場合 staticメソッドの場合
target クラスのプロトタイプ クラス(コンストラクタ関数)
propertyKey メソッド名 メソッド名
descriptor PropertyDescriptorのオブジェクト PropertyDescriptorのオブジェクト

メソッドデコレータ関数は、PropertyDescripterオブジェクトをreturnすることで、対象メソッドのPropertyDescriptorを上書きすることができました。

PropertyDescriptorオブジェクトの属性をまとめます。

属性名 説明
configurable プロパティフラグの変更やプロパティの削除を許可するか否か
enumerable プロパティを列挙可能にするかどうか
value 対象のメソッドそのもの
writable プロパティの値書き込み(再代入)を許可するかどうか
get 対象メソッドのgetter(value, writableとは同居できない)
set 対象メソッドのsetter(value, writableとは同居できない)

本記事のサンプルコードはGitHubに上げています。

以上。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0