🍰

もう怖くないTypeScriptのDecorator機能

2022/02/24に公開1

はじめに

Nest.js を始める前に肝となる TS の Decorator 機能について学習したほうがよいと思い、学習したときのメモです。

Decorator 機能を知りたい!っていう方、Nest.js のように Decorator 機能を使ったライブラリをこれから使おうとしている(もしくはもう使っている)方の参考になれば幸いです。

今回、筆者のように TS のクラス文に慣れていない方にも読みやすい記事にしました(のつもり)。ですので、複雑な型定義を避けたり、プロパティなどの説明を簡単な表現に置き換えています。

Decorator とは

Decorator(デコレータ)はクラスの宣言などに結び付けられる特別な宣言の 1 つ。@expression の形式で宣言する。既存のクラスやメソッドにデコレーションする(=追加機能を入れる)イメージでしょうか。

Decorator の設置はこんな感じ。@fuga(true) みたいに引数をセットすることも出来ます。

@hoge
class Fuga {
  fuga: string;

  constructor(f: string) {
    this.fuga = t;
  }
}

ちなみに TS だけでなく、Python など他の言語にも存在する機能です。

Decorator Factories: デコレータ・ファクトリー

Decorator Factories(デコレータ・ファクトリー)とは、設置したデコレータによって呼び出される式を返すシンプルな関数です。デコレータによって適用される方法をカスタマイズしたい場合に作成します。

コード例は以下のとおりです。ここで targetvalue の 2 変数が登場しますが、ここはデコレータの設置場所によって、意味合いが変わっておくので一旦ここでは置いておきます。

// Decorator Factoryの宣言
function factoryMethod(value: string) {
  // Decoratorによって呼び出される関数
  return function (target) {
    // `target`と`value`使って処理の内容を定義
  };
}

Decorator 機能の始め方

ローカル環境で Decorator 機能を始める際は、少し設定が必要となります。

まず初期化してから始める場合は、このコマンドを叩いてください。

$ tsc --target ES5 --experimentalDecorators

既存の TS 環境を使って始める場合は、tsconfig.json の変更が必要です。

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

公式ドキュメントページはこちらです。
https://www.typescriptlang.org/docs/handbook/decorators.html#introduction

なお、ブラウザで TS を動作できる公式 Playgroundでは、デフォルトで上記の設定がすでにされているため、そのまま動かすことが可能です。

Decorator を付けられる場所

付けられる場所は以下の通りです。

  • クラス宣言 -> Class Decorators: クラス・デコレータ
  • メソッド -> Method Decorators: メソッド・デコレータ
  • アクセサ -> Accessor Decorators: アクセサ・デコレータ
  • プロパティ -> Property Decorators: プロパティ・デコレータ
  • パラメータ -> Parameter Decorators: パラメーター・デコレータ

今回は使用機会の多い Class Decorators / Method Decorators / Property Decorators の 3 つについて、順番に概要/実装方法を紹介していきます。

Decorator の評価順

各 Decorator の紹介前に、Decorator の評価順についてサラッと触れておきます。
順番は Decorator が呼び出される場所によって厳格に決まっているので、注意しましょう。

  1. クラスのインスタンスへ適用
    1. Parameter Decorators
    2. Method Decorators
    3. Accessor Decorators
    4. Property Decorators
  2. クラスの Static メンバーへ適用
    1. Parameter Decorators
    2. Method Decorators
    3. Accessor Decorators
    4. Property Decorators
  3. クラスのコンストラクタ関数
    1. Parameter Decorators
  4. クラス宣言
    1. Class Decorators

各 Decorator の紹介

Class Decorators: クラス・デコレータ

Class Decorator(クラス・デコレータ)は、クラスの定義の検査/修正/置換が出来ます。クラス宣言の直前で宣言し、クラスのコンストラクタに適用されます。

例えば BaseEntity という各クラスのベース用クラスを設置し、各クラスを extends していくとしましょう。

class BaseEntity {
  readonly id: number;
  readonly created: string;
  constructor() {
    this.id = Math.random();
    this.created = new Date().toLocaleDateString();
    this.updated = new Date().toLocaleDateString();
  }
}

class Course extends BaseEntity {
  constructor(public name: string) {
    super();
  }
}
class Subject extends BaseEntity {
  constructor(public name: string) {
    super();
  }
}
class Student extends BaseEntity {
  constructor(public name: string) {
    super();
  }
}

const mathCourse = new Course("English");
console.log("id: " + mathCourse.id);
console.log("created: " + mathCourse.created);

const john = new Student("John Doe");
// ...

ただし、この書き方だと 1 つ問題があります。BaseEntity クラスから extends しているクラスの数が増えて来た場合です。

宣言する度に何度も extendssuper() する必要が出てします。ベースクラスを使う以上、もう少し良い方法はないのでしょうか。ここで Class Decorator の出番です。先程のコードを Class Decorator を使用した形に書き換えてみましょう[1]

// Decorator Factoriesを定義
const BaseEntity = (ctr: Function) => {
  ctr.prototype.id = Math.random();
  ctr.prototype.created = new Date().toLocaleString("es-ES");
};

@BaseEntity
class Course {
  constructor(public name: string) {}
}

@BaseEntity
class Subject {
  constructor(public name: string) {}
}

@BaseEntity
class Student {
  constructor(public name: string) {}
}

// 型エラーが出るため、@ts-ignoreしている
const mathCourse = new Course("English");
// @ts-ignore
console.log("id: " + mathCourse.id);
// @ts-ignore
console.log("created: " + mathCourse.created);

const john = new Student("John Doe");
// ...

@BaseEntity をクラス宣言時に設置するだけで、クラスの拡張が可能になりました。実行も出来ます。

ただし、1 点注意が必要なのはインスタンスなどでデコレータで定義したクラス用プロパティを呼ぼうとすると、TS にデコレータによってクラスが拡張されていることが伝わっていないため、型エラーが発生します。これは長年 GitHub の issue にも上がっており、直近のコメントでは根本的な解決にもう少し時間がかかるのではないかという話も浮上しています。興味のある方は覗いてみてください。

https://github.com/microsoft/TypeScript/issues/4881

Method Decorators: メソッド・デコレータ

Method Decorators(メソッド・デコレータ)は、メソッドの定義の検査/修正/置換が出来ます。メソッド定義の直前で宣言します。

Method Decorators の Decorator Factories 定義は少し特殊です。以下の 3 引数が必要となります。

  • target: クラスのコンストラクタのメソッド本体。使う機会は少ない。
  • propertyKey: デコレータを設置するメソッドの名前。
  • descriptor: デコレータを設置するメソッドのProperty Descriptor。今回はメソッドを呼び出せるものと覚えてもらえばいいです。

Method Decorators の Decorator Factories のコード例はこちら。
デコレータを設置するメソッドの処理内容を上書きすることを強調するため、ある条件になったとき、元メソッドの実行からさらに掛け算を実行する checkCalculate を作成しました。

function checkCalculate(num: number) {
  return (
    _target: Object,
    _propertyKey: string,
    descriptor: PropertyDescriptor
  ) => {
    const addFunc = descriptor.value;
    descriptor.value = function (...args: number[]) {
      // apply() メソッドを使って、デコレータを設置する元メソッドを呼び出す
      const result = addFunc.apply(this, args);
      // 元メソッドの返却値が10未満のときは、掛け算を実行する
      return result < 10 ? result * num : result;
    };
  };
}

では早速 checkCalculate を使ってみましょう。

class Calculate {
  @checkCalculate(10)
  sum(a: number, b: number): number {
    return a + b;
  }
}

// 50 -> 1 + 4 = 5を実行した後に、5 < 10だったため更に5 x 10 = 50を返す
console.log(new Calculate().sum(1, 4));
// 21 -> 20 + 1 = 21を実行した後に、21 > 10だったため、掛け算は実行せずそのまま21を返す
console.log(new Calculate().sum(20, 1));

Property Decorators: プロパティ・デコレータ

Property Decorators(プロパティ・デコレータ)は、プロパティの定義の検査/修正/置換が出来ます。クラスのプロパティ宣言の直前で宣言します。

Property Decorators の Decorator Factories 定義では、以下の 2 引数を使います。

  • target: デコレータを設置するプロパティがクラスの静的プロパティの場合、クラスの constructor メソッドを表す。それ以外のプロパティでは、クラスのプロトタイプ。
    • 静的プロパティについてはこちらを参照。
  • memberName: デコレータを設置するプロパティの名前。

Property Decorators の Decorator Factories のコード例はこちら。
クラスプロパティへ notAllowlist にない値がセットされたときのみ、プロパティ値が変更される allowNameOnly を作成しました。

const notAllowlist = ["Jone Doe", "Peter Paker"];

const allowNameOnly = (target: any, memberName: string) => {
  let currentValue: string = target[memberName];

  Object.defineProperty(target, memberName, {
    set: (newValue: string) => {
      if (notAllowlist.includes(newValue)) {
        return;
      }
      currentValue = newValue;
    },
    get: () => currentValue,
  });
};

class Person {
  @allowNameOnly
  name: string = "Jon";
}

const person = new Person();
console.log(person.name); // Jon

person.name = "Jone Doe";
console.log(person.name); // Jon

person.name = "Peter Paker";
console.log(person.name); // Jon

person.name = "Mary Jane";
console.log(person.name); // Mary Jane

まとめ

今回は TS の Decorator 機能についてまとめてみました。まだ実験段階の機能ということもあり、多少不安定な箇所もありますが、いざ使ってみるとクラス文を使う上でとても便利な機能であることが伝わってきたはずです。また曖昧になりがちな生 JS のクラス文の知識も少しずつ積み重なっていくことでしょう。

安定版がいつか出ることを願いつつ、Decorator をどんどん書いていきましょう。

参考ページ

脚注
  1. 公式では Decorator Factories の型定義が、<T extends { new (...args: any[]): {} }> で記載されていますが、本記事では TS に慣れていない方でもわかりやすいように Function で定義する方法を取らせていただきました。公式ページはこちら → https://www.typescriptlang.org/docs/handbook/decorators.html#class-decorators。 ↩︎

GitHubで編集を提案

Discussion