デコレータ (古い構文)

前の章では、2022 年に標準として採用されたデコレータの標準構文を紹介しました。しかし、それ以前は、TypeScript は 2014 年の時点でデコレータをサポートしていましたが、古い構文を使用していました。

デコレータの古い構文は、標準の構文とは大きく異なります。古い構文は将来的に削除される予定ですが、多くの既存のプロジェクトでは依然として古い構文が使用されています。

ExperimentalDecorators コンパイル オプション

デコレーターの古い構文を使用するには、--experimentalDecorators コンパイル オプションをオンにする必要があります。

$ tsc --target ES5 --experimentalDecorators

さらに、別のコンパイル オプション --emitDecoratorMetadata があります。これは、他のツールまたは特定のモジュール (reflect-metadata など) で使用するデコレータ メタデータを生成するために使用されます。

これら 2 つのコンパイル オプションは、コマンド ラインまたは tsconfig.json ファイルで設定できます。

{
  "コンパイラーオプション": {
    "ターゲット": "ES6",
    "experimentalDecorators": true"emitDecoratorMetadata": true
  }
}

デコレータの種類

デコレータは、装飾するさまざまなオブジェクトに応じて 5 つのカテゴリに分類できます。

  • クラス デコレータ: クラスに使用されます。
  • プロパティ デコレータ: プロパティに使用されます。
  • メソッド デコレータ: メソッドに使用されます。
  • アクセサ デコレータ: クラスの set メソッドまたは get メソッドに使用されます。
  • パラメータ デコレータ: メソッドのパラメータに使用されます。

これら 5 つのデコレータを組み合わせて使用​​する例を次に示します。

@ClassDecorator() // (A)
クラスA {

  @PropertyDecorator() // (B)
  名前: 文字列;

  @MethodDecorator() //(C)
  飛ぶ(
    @ParameterDecorator() // (D)
    メートル: 数値
  ) {
    // コード
  }

  @AccessorDecorator() // (E)
  getegg() {
    // コード
  }
  setegg(e) {
    // コード
  }
}

上記の例では、A はクラス デコレータ、B はプロパティ デコレータ、C はメソッド デコレータ、D はパラメータ デコレータ、E はアクセサ デコレータです。

コンストラクターにはメソッド デコレーターはなく、パラメーター デコレーターのみがあることに注意してください。クラス デコレータは実際に構築メソッドを装飾します。

さらに、デコレータはクラス全体またはクラスの内部メンバーのいずれかでのみ使用でき、独立した関数では使用できません。

関数デコレーター() {
  console.log('デコレータ内');
}

@Decorator // エラーレポート
装飾された関数() {
  console.log('装飾済み');
}

上記の例では、デコレータは通常の関数に使用されていますが、これは無効であり、エラーが発生します。

クラスデコレータ

クラス デコレータはクラスに適用されますが、実際にはクラスのコンストラクタに適用されます。

クラス デコレータの唯一のパラメータは構築メソッドです。デコレータ内の構築メソッドにはさまざまな変更を加えることができます。クラス デコレータに戻り値がある場合、元のコンストラクタは置き換えられます。

クラスデコレータの型定義は以下のとおりです。

type ClassDecorator = <TFunction extends Function>
  (ターゲット: TFunction) => TFunction | 無効

上記の定義では、型パラメーター TFunction は関数でなければならず、実際にはコンストラクターです。クラス デコレータの戻り値は、処理された元のコンストラクタを返すか、新しいコンストラクタを返します。

ここに例を示します。

関数 f(ターゲット:任意) {
  console.log('デコレータを適用')
  リターンターゲット。
}

@f
クラス A {}
// 出力: デコレータを適用する

上記の例では、デコレータ @f が使用されているため、クラス A のコンストラクタは自動的に f を渡します。

クラス A は新しいインスタンスを作成する必要はなく、デコレータも実行されます。デコレータは実行時ではなく、コードの読み込みフェーズ中に実行され、一度だけ実行されます。

TypeScript にはコンパイル フェーズがあるため、デコレータによってクラスの動作に加えられた変更は、実際にはコンパイル フェーズ中に発生します。これは、TypeScript デコレータがコンパイル時にコードを実行できること、つまり、本質的にはコンパイル時に実行される関数であることを意味します。

別の例を見てみましょう。

@sealed
クラス バグレポート {
  タイプ = "レポート";
  タイトル: 文字列;
 
  コンストラクター(t:string) {
    this.title = t;
  }
}

function sealed(コンストラクター: 関数) {
  Object.seal(コンストラクター);
  Object.seal(constructor.prototype);
}

上記の例では、デコレータ @sealed()BugReport クラスをロックして、静的メンバーやインスタンス メンバーを追加または削除できないようにします。

クラス デコレータがコンストラクタ メソッドに加えて他のパラメータを必要とする場合は、「ファクトリ モード」を採用できます。つまり、他のパラメータを受け入れ、実行後にデコレータを返すことができる関数内にデコレータを記述します。ただし、これには、デコレータを呼び出すときにファクトリ関数を最初に実行する必要があります。

関数ファクトリー(情報:文字列) {
  console.log('受信: ', 情報);
  return 関数 (ターゲット:任意) {
    console.log('デコレータを適用');
    リターンターゲット。
  }
}

@factory('何かをログに記録')
クラス A {}

上記の例では、関数 factory() の戻り値はデコレータであるため、デコレータをロードするときは、最初に @factory('log something') を実行してデコレータを取得する必要があります。この利点は、追加パラメータ (この場合はパラメータ info) を追加できることです。

つまり、「@」の後には関数名または関数式が続き、次のようなコードを書くこともできます。

@((コンストラクター:関数) => {
  console.log('何かをログに記録します');
})
class InlineDecoratorExample {
  // ...
}

上の例では、「@」の後にアロー関数が続いていますが、これも正当です。

クラス デコレータに戻り値を持たせることはできません。戻り値がある場合は、装飾されたクラスのコンストラクタが置き換えられます。 JavaScript クラスはコンストラクターの糖衣構文と同等であるため、デコレーターは通常、元のクラスを変更または拡張するために新しいクラスを返します。

関数デコレーター(ターゲット:任意) {
  戻りクラスはターゲットを拡張します {
    値 = 123;
  };
}

@デコレータ
クラス Foo {
  値 = 456;
}

const foo = 新しい Foo();
コンソール.log(foo.value); // 123

上の例では、デコレータ decorator は、元のクラスを置き換えて新しいクラスを返します。

上記の例のデコレータ パラメータ target はタイプ any であり、より正確なコンストラクタ メソッドに変更できます。

タイプコンストラクター = {
  new(...引数: 任意[]): {}
};

関数デコレータ<T extends コンストラクタ> (
  ターゲット:T
) {
  戻りクラスはターゲットを拡張します {
    値 = 123;
  };
}

このときのデコレータの動作は以下の通りです。

@デコレータ
クラス A {}

// と同等
クラス A {}
A = デコレータ(A) || A;

上記のコードでは、デコレータは新しいクラス A を返すか、値を返さず、A はデコレータによって処理された状態を維持します。

メソッドデコレータ

メソッド デコレーターは、クラスのメソッドを装飾するために使用されます。その型は次のように定義されます。

type MethodDecorator = <T>(
  ターゲット: オブジェクト、
  propertyKey: 文字列|シンボル、
  記述子: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;

メソッド デコレータは、合計 3 つのパラメータを受け入れることができます。

  • ターゲット: (クラスの静的メソッドの場合) クラスのコンストラクター、または (クラスのインスタンス メソッドの場合) クラスのプロトタイプ。
  • propertyKey: 装飾されたメソッドのメソッド名。タイプは string|symbol です。
  • descriptor: 装飾されたメソッドの説明オブジェクト。

メソッド デコレーター (存在する場合) の戻り値は、メソッドの変更された説明オブジェクトであり、元のメソッドの説明オブジェクトを上書きする可能性があります。

ここに例を示します。

関数 enumerable(値: ブール値) {
  戻り関数 (
    ターゲット: 任意、
    プロパティキー: 文字列、
    記述子: プロパティ記述子
  ) {
    記述子.enumerable = 値;
  };
}

クラス グリーター {
  挨拶: 文字列;

  コンストラクター(メッセージ:文字列) {
    this.greeting = メッセージ;
  }
 
  @enumerable(false)
  挨拶する() {
    return 'Hello, ' + this.greeting;
  }
}

上記の例では、メソッド デコレータ @enumerable() が Greeter クラスの greet() メソッドを修飾し、その機能はメソッドの記述オブジェクトの traversability 属性 enumerable を変更することです。 @enumerable(false) は、メソッドをトラバース不可に変更することを意味します。

別の例を見てみましょう。

関数ロガー(
  ターゲット: 任意、
  プロパティキー: 文字列、
  記述子: プロパティ記述子
) {
  const オリジナル = 記述子.値;

  descriptor.value = function (...args) {
    console.log('params: ', ...args);
    const result = original.call(this, ...args);
    console.log('結果: ', 結果);
    結果を返します。
  }
}

クラスC {
  @ロガー
  add(x:数値, y:数値) {
    x + y を返します。
  }
}

(新しい C()).add(1, 2)
// パラメータ: 1 2
// 結果: 3

上記の例では、メソッド デコレータ @logger を使用して add() メソッドを装飾しており、その機能はメソッドがログを出力できるようにすることです。 「add()」が呼び出されるたびに、コンソールはパラメータと結果を出力します。

プロパティデコレーター

プロパティ デコレータはプロパティを装飾するために使用され、その型は次のように定義されます。

タイプ PropertyDecorator =
  (
    ターゲット: オブジェクト、
    propertyKey: 文字列|シンボル
  ) => 無効;

プロパティ デコレーター関数は 2 つのパラメーターを受け入れます。

  • ターゲット: (プロパティの場合) クラスのプロトタイプ オブジェクト、または (静的プロパティの場合) クラスのコンストラクター。
  • propertyKey: 装飾された属性の属性名。タイプは文字列またはシンボル値であることに注意してください。

プロパティ デコレータは値を返す必要はありませんが、値があったとしても無視されます。

ここに例を示します。

function ValidRange(最小:数値, 最大:数値) {
  return (ターゲット:オブジェクト、キー:文字列) => {
    Object.defineProperty(ターゲット, キー, {
      セット: 関数(v:数値) {
        if (v < 最小 || v > 最大) {
          throw new Error(`許可されていない値 ${v}`);
        }
      }
    });
  }
}

// 年に ValidRange をインストールする出力
クラス学生{
  @ValidRange(1920, 2020)
  年!: 数値;
}

const スタッド = 新しい学生();

// エラー 許可されていない値 2022
スタッドイヤー = 2022;

上記の例では、デコレーター ValidRange は属性 year の上限および下限チェッカーを設定します。属性に値が割り当てられたときに上限および下限を超えている限り、エラーが報告されます。

プロパティ デコレータの最初のパラメータ (インスタンス プロパティなど) は、インスタンス オブジェクト (つまり、「this」 オブジェクト) ではなく、クラスのプロトタイプ オブジェクトであることに注意してください。これは、デコレーターが実行されるとき、クラスはまだ新しいインスタンスを作成していないため、インスタンス オブジェクトは存在しません。

「this」を取得できないため、プロパティ デコレータはインスタンス プロパティの値を取得できません。これは、パラメータに属性記述オブジェクトを提供しない理由でもあります。

関数 logProperty(ターゲット: オブジェクト、メンバー: 文字列) {
  const prop = Object.getOwnPropertyDescriptor(ターゲット, メンバー);
  console.log(`プロパティ ${メンバー} ${prop}`);
}

class PropertyExample {
  @logProperty
  名前:文字列 = 'Foo';
}
// 出力プロパティ名未定義

上記の例では、プロパティ デコレータ @logProperty は内部的にインスタンス プロパティ name のプロパティ記述オブジェクトを取得しようとしていますが、結果は unknown になります。上記の例の target はインスタンス オブジェクトではなく、クラスのプロトタイプ オブジェクトであるため、name 属性は取得できません。これは、target.name が存在しないことを意味します。したがって、取得されるものは unknown になります。 name属性はthis.name を通じてのみ取得できますが、this` はまだ存在しません。

プロパティ デコレーターはインスタンス プロパティの値を取得できないだけでなく、インスタンス プロパティを初期化または変更できないだけでなく、その戻り値も無視されます。したがって、その有用性は限られています。

ただし、プロパティ デコレーターが現在のプロパティのアクセサー (ゲッター/セッター) を設定する場合、インスタンス プロパティはコンストラクターで読み書きできます。

関数 Min(制限:数値) {
  戻り関数(
    ターゲット: オブジェクト、
    プロパティキー: 文字列
  ) {
    let 値: 文字列;

    const getter = function() {
      戻り値;
    };

    const setter = function(newVal:string) {
      if(newVal.length < 制限) {
        throw new Error(`パスワードは ${limit}` より大きい必要があります);
      }
      それ以外 {
        値 = 新しいVal;
      }
    };
    Object.defineProperty(target, propertyKey, {
      取得: ゲッター、
      セット: セッター
    });
  }
}

クラス ユーザー {
  ユーザー名: 文字列;
  
  @みん(8)
  パスワード: 文字列;
  
  コンストラクター(ユーザー名: 文字列、パスワード: 文字列){
    this.username = ユーザー名;
    this.password = パスワード;
  }
}

const u = 新しいユーザー('Foo', 'pass');
// エラーを報告する パスワードは 8 より大きい必要があります

上記の例では、プロパティ デコレーター @Min はアクセサーを設定することでインスタンス プロパティの値を取得します。

アクセサデコレータ

アクセサ デコレータは、クラスのアクセサを装飾するために使用されます。いわゆる「アクセサー」とは、特定の属性のゲッターとセッターを指します。

アクセサ デコレータの型定義はメソッド デコレータと一致しています。

type AccessorDecorator = <T>(
  ターゲット: オブジェクト、
  propertyKey: 文字列|シンボル、
  記述子: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;

アクセサ デコレータは 3 つのパラメータを取ります。

  • target: (静的プロパティへのアクセサーの場合) クラスのコンストラクター、または (インスタンス プロパティへのアクセサーの場合) クラスのプロトタイプ。
  • propertyKey: アクセサーのプロパティ名。
  • descriptor: アクセサーの属性説明オブジェクト。

アクセサー デコレーターの戻り値 (存在する場合) は、プロパティの新しい説明オブジェクトとして使用されます。

ここに例を示します。

設定可能な関数(値: ブール値) {
  戻り関数 (
    ターゲット: 任意、
    プロパティキー: 文字列、
    記述子: プロパティ記述子
  ) {
    記述子.configurable = 値;
  };
}

クラスポイント{
  プライベート _x: 数値;
  プライベート _y: 数値;
  コンストラクター(x:数値, y:数値) {
    this._x = x;
    this._y = y;
  }
 
  @configurable(false)
  get x() {
    これを返します。_x;
  }
 
  @configurable(false)
  y()を取得 {
    これを返します。_y;
  }
}

上記の例では、デコレータ @configurable(false) は、装飾されたプロパティ (x および y) のプロパティ記述オブジェクトの configurable キーをオフにします (つまり、プロパティの構成可能性をオフにします)。 )。

次の例では、デコレーターを使用して属性値を検証し、割り当てられた値が条件を満たさない場合にエラーを報告します。

関数バリデータ(
  ターゲット: オブジェクト、
  プロパティキー: 文字列、
  記述子: プロパティ記述子
){
  constoriginalGet = 記述子.get;
  constoriginalSet = 記述子.set;
  
  if (オリジナルセット) {
    descriptor.set = 関数 (val) {
      if (val > 100) {
        throw new Error(`${propertyKey}` の値が無効です);
      }
      オリジナルセット.call(this, val);
    };
  }
}

クラスC {
  #foo!: 数値;

  @検証者
  set foo(v) {
    this.#foo = v;
  }

  get foo() {
    これを返します。#foo;
  }
}

const c = 新しい C();
c.foo = 150;
// エラーを報告する

上記の例では、デコレータは元の値レジスタを独自の定義された値レジスタに置き換え、検証条件を追加します。

TypeScript では、同じプロパティのアクセサー (ゲッターとセッター) に同じデコレーターを使用することはできません。つまり、2 つのアクセサーのうち 1 つだけを装飾できます。それは前にあるアクセサーでなければなりません。そうでない場合は、エラーが報告されます。

// エラーを報告する
クラス人 {
  #名前:文字列;

  @デコレータ
  名前を設定(n:文字列) {
    this.#name = n;
  }

  @Decorator // エラーレポート
  名前を取得() {
    これを返します。#name;
  }
}

上記の例では、@Decoratorname 属性の valuer と valuer の両方を修飾しているため、エラーが報告されます。

ただし、以下の書き方ではエラーになりません。

クラス人 {
  #名前:文字列;

  @デコレータ
  名前を設定(n:文字列) {
    this.#name = n;
  }
  名前を取得() {
    これを返します。#name;
  }
}

上記の例では、@Decorator はその後に最初に現れる値レジスタを修飾するだけであり (set name())、値を修飾しない (get name()) ため、エラーは報告されません。

同じプロパティの値ストアラーと値ストアラーの両方にデコレーターを使用できない理由は、デコレーターはプロパティ記述オブジェクトから値ストアラーと値ストアラーを同時に取得できるため、一度呼び出すだけで済むためです。 。

パラメータデコレータ

パラメーター デコレーターは、コンストラクターまたはその他のメソッドのパラメーターを装飾するために使用されます。その型は次のように定義されます。

タイプ ParameterDecorator = (
  ターゲット: オブジェクト、
  propertyKey: 文字列|シンボル、
  パラメータインデックス: 数値
) => 無効;

パラメータ デコレータは 3 つのパラメータを受け入れます。

  • ターゲット: (静的メソッドの場合) クラスのコンストラクター、または (クラスのメソッドなど) クラスのプロトタイプ オブジェクト。
  • propertyKey: 修飾されたメソッドの名前。タイプは string|symbol です。 -parameterIndex: メソッドのパラメータ シーケンス内の現在のパラメータの位置 (0 から始まります)。

このデコレータには戻り値は必要ありませんが、戻り値がある場合でも無視されます。

ここに例を示します。

関数ログ(
  ターゲット: オブジェクト、
  propertyKey: 文字列|シンボル、
  パラメータインデックス: 数値
) {
  console.log(`${String(propertyKey)} NO.${parameterIndex} パラメータ`);
}

クラスC {
  メンバー(
    @log x:数値、
    @log y:番号
  ) {
    console.log(`メンバーパラメータ: ${x} ${y}`);
  }
}

const c = 新しい C();
c.メンバー(5, 5);
// メンバーNO.1パラメータ
// メンバーNO.0パラメータ
// メンバーパラメータ: 5 5

上記の例では、パラメータ デコレータはパラメータの位置番号を出力します。以下のパラメータが最初に出力されることに注意してください。

他のデコレータとは異なり、パラメータ デコレータは主に情報を出力するために使用され、クラスの動作を変更する方法はありません。

デコレータの実行順序

前述したように、デコレータは 1 回だけ実行されます。つまり、新しいインスタンスを作成するためにクラスが呼び出されていない場合でも、デコレータは実行され、再度実行されることはありません。

デコレータを実行する場合は、以下の順序で実行してください。

  1. インスタンス関連のデコレータ。
  2. 静的関連のデコレータ。
  3. コンストラクター メソッドのパラメーター デコレーター。
  4. クラスデコレーター。

以下の例を参照してください。

関数 f(キー:文字列):任意の {
  戻り関数 () {
    console.log('実行:', key);
  };
}

@f('クラスデコレータ')
クラスC {
  @f('静的メソッド')
  静的メソッド() {}
  
  @f('インスタンスメソッド')
  方法() {}

  constructor(@f('コンストラクター パラメーター') foo:any) {}
}

上記の例をロードすると、出力は次のようになります。

実行: インスタンスメソッド
実行: 静的メソッド
実行: コンストラクターのパラメーター
実行: クラスデコレータ

同じレベルのデコレータの実行順序は、コードの順序に基づきます。ただし、パラメータ デコレータは常にメソッド デコレータより前に実行されます。

関数 f(キー:文字列):任意の {
  戻り関数 () {
    console.log('実行:', key);
  };
}

クラスC {
  @f('方法 1')
  m1(@f('パラメータ 1') foo:any) {}

  @f('プロパティ 1')
  p1: 数値;

  @f('方法 2')
  m2(@f('パラメータ 2') foo:any) {}

  @f('プロパティ 2')
  p2: 数値;
}

上記の例をロードすると、出力は次のようになります。

実行: パラメータ 1
実行: 方法 1
実行: プロパティ 1
実行: パラメータ 2
実行: 方法 2
実行: プロパティ 2

上記の例では、インスタンス デコレータの実行順序はコードの順序に完全に一致しています。ただし、同じメソッドのパラメータ デコレータは、常にそのメソッドのメソッド デコレータよりも先に実行されます。

同じメソッドまたはプロパティに複数のデコレータがある場合、デコレータは順番にロードされ、逆の順序で実行されます。

関数 f(キー:文字列):任意の {
  console.log('読み込み中:', key);
  戻り関数 () {
    console.log('実行:', key);
  };
}

クラスC {
  @f('A')
  @f('B')
  @f('C')
  m1() {}
}
// ロード: A
//ロード: B
// ロード: C
// 実行:C
// 実行:B
// 実行:A

同じメソッドに複数のパラメータがある場合、パラメータは順番にロードされ、逆の順序で実行されます。

関数 f(キー:文字列):任意の {
  console.log('読み込み中:', key);
  戻り関数 () {
    console.log('実行:', key);
  };
}

クラスC {
  方法(
    @f('A') a:任意、
    @f('B') b:任意、
    @f('C') c:any、
  ) {}
}
// ロード: A
//ロード: B
// ロード: C
// 実行:C
// 実行:B
// 実行:A

関数ではデコレータを使用できないのはなぜですか?

デコレータはクラスとクラス メソッドにのみ使用でき、関数には使用できません。主な理由は、関数ホイスティングが存在するためです。

JavaScript 関数は、どこにあってもコードの先頭に昇格されます。

addOne(1);
関数 addOne(n:number) {
  n + 1 を返します。
}

上記の例では、関数 addOne() は定義される前に実行されるため、エラーは報告されません。その理由は、関数がプロモートされ、自動的にコードの先頭にプロモートされるためです。

通常の関数でデコレータの使用が許可されている場合、予期しない動作が発生する可能性があります。

カウンタ = 0 とします。

let add = function (target:any) {
  カウンタ++;
};

@追加
関数 foo() {
  //...
}

上記の例では、デコレータ @add が使用されるたびに変数 counter1 ずつインクリメントされるのが本来の意図ですが、実際には関数プロモーションの存在によりエラーが報告されます。実際に実行されるコードは以下のようになることがわかります。

@add // エラーレポート
関数 foo() {
  //...
}

カウンタ = 0 とします。
let add = function (target:any) {
  カウンタ++;
};

上記の例では、@add が定義される前に呼び出されるため、エラーが報告されます。

つまり、関数の巻き上げのため、デコレーターを関数で使用することはできません。クラスの昇格はありませんので問題ありません。

一方、関数を修飾する必要がある場合は、高階関数の形式で直接実行でき、デコレーターとして記述する必要はありません。

関数 doSomething(名前) {
  console.log('こんにちは' + 名前);
}

関数loggingDecorator(ラップされた) {
  戻り関数() {
    console.log('開始');
    const result = ラップされた.apply(this, argument);
    console.log('完了');
    結果を返します。
  }
}

const ラップ =loggingDecorator(doSomething);

上の例では、loggingDecorator() はデコレータです。元の関数が実行のために渡される限り、デコレータの効果を持つことができます。

複数のデコレーターの合成

複数のデコレータを同じターゲット オブジェクトに適用でき、1 行に記述できます。

@f @gx

上記の例では、デコレータ @f@g がターゲット オブジェクト x を同時に装飾します。

複数のデコレータを複数行に記述することもできます。

@f
@g
×

複数のデコレータの効果は、関数の合成に似ており、内側から外側へ順番に実行されます。上の例では、「f(g(x))」を実行しています。

前に述べたように、fg が式の場合、最初に外側から内側に向​​かって評価する必要があります。

参考リンク


作者: wangdoc

アドレス: https://wangdoc.com/

ライセンス: クリエイティブ・コモンズ 3.0