ES5変換後のコードを見てTypeScriptのデコレータを理解する

TypeScript・JavaScriptのデコレータは、クラス宣言やそのメソッドなどにアタッチできる特別な宣言です。
公式ドキュメント: https://www.typescriptlang.org/docs/handbook/decorators.html  

使い方としては`@hoge`のようなものを追加するだけという魔法のような機能なのですが、いまいち動き方のイメージがしづらくないでしょうか？  

今回はメソッドデコレータを例に、ES5へのトランスパイル後のソースを見つつ、「内部的にどう処理されているのか？」にフォーカスして紹介していきます。

デコレータそのものについては説明していないので、「そもそもデコレータとは？」という方には下記の記事をおすすめします。
[TypeScriptによるデコレータの基礎と実践](https://qiita.com/taqm/items/4bfd26dfa1f9610128bc)

## ClassからPrototypeへの変換を見てみる
デコレータの挙動を知るためには、前提としてJavaScriptのPrototypeについて理解しておく必要があります。

ということで、まずは「Class構文をES5へトランスパイルしたときに、どんな感じのPrototypeの構文に書き換えられるか」を追っていきましょう。

#### TypeScriptのソース
```typescript:index.ts
class Person {
  name: string = 'Some Name';

  sayHello() {
    console.log('Hello.');
  }

  sayBye() {
    console.log('Bye.');
  }
}
```

#### トランスパイル後のJavaScript
```javascript:index.js
"use strict";
var Person = /** @class */ (function () {
  function Person() {
    this.name = 'Some Name';
  }
  Person.prototype.sayHello = function () {
    console.log('Hello.');
  };
  Person.prototype.sayBye = function () {
    console.log('Bye.');
  };
  return Person;
}());
```

グローバルに定義されているPerson関数がクロージャとして内部で別のPerson関数を返しています。
そして、Person関数のオブジェクトは、prototypeを通じて`sayHello`と`sayBye`を実行できるような作りになっています。

ここで理解しておく必要があるのは、

- `name`はPersonオブジェクトのプロパティであるが、
- `sayHello`と`sayBye`はPersonオブジェクトのメソッドではなく、**Person.prototypeオブジェクトのメソッドである**

ということです。

## メソッドデコレータの基本的な動きを解析する

それを踏まえた上で、Personクラスのメソッドにデコレータを追加して、デコレータが内部的にどんな動きをしているのかを確認してみましょう。

#### TypeScriptのソース
下記は、メソッドデコレータを先ほどのPersonクラスに追加したものです。
デコレータはログ収集に使われることが多いみたいなので、便宜的にloggerという名前にしています。

```typescript:index.ts
class Person {
  name: string = 'Some Name';

  @logger // メソッドデコレータを追加
  sayHello() {
    console.log('Hello.');
  }

  sayBye() {
    console.log('Bye.');
  }
}

// デコレータ
function logger(target: any, key: string) {
  console.log(target);
  console.log(key);
}
```

メソッドデコレータは、第一引数に`target`、第二引数に`key`、第三引数に`descriptor`（今回は省略）を受け取ります。

#### 実行結果
このファイルを実行してみると、

```text
Person { greet: [Function], sayBye: [Function] }
sayHello
```

と出力されます。
第一引数の`target`にはTypeScript上でのPersonクラスが持っているメソッド、
第二引数の`key`にはデコレータの対象としている関数名（`sayHello`）が入ってきます。
（第三引数は今回省略していますが、`Object.getOwnPropertyDescriptor`によって取得できるオブジェクトが受け取れます。）

ここで注目すべきは、`target`には`name`プロパティは含まれないことです。
この理由は、**`target`として渡されるものが、「トランスパイル後のPerson関数のPrototype」だから**です。

#### 解説
この挙動に関しては、デコレータを追加したTypeScriptのトランスパイル後コードを見てみれば分かります。

```javascript:index.js
"use strict";
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 Person = /** @class */ (function () {
    function Person() {
        this.name = 'Some Name';
    }
    Person.prototype.sayHello = function () {
        console.log('Hello.');
    };
    Person.prototype.sayBye = function () {
        console.log('Bye.');
    };
    // ↓↓↓ここに注目↓↓↓
    __decorate([
        logger
    ], Person.prototype, "sayHello", null);
    return Person;
}());
function logger(target, key) {
    console.log(target); // Person { greet: [Function], sayBye: [Function] }
    console.log(key); // sayHello
}
```

`__decorate`関数が定義され、Person関数内で実行されていますね。
そして、`Person.prototype`がその第二引数に入っています。

`__decorate`関数自体は、ごちゃごちゃといろいろ書いてありますが、今回の理解に必要な部分だけ抜き出すと、

```javascript
var __decorate = function (decorators, target, key, desc) {
  for (var i = decorators.length - 1; i >= 0; i--) {
    decorators[i](target, key);
  }
}
```

こんな感じになります。

今回の例で言えば、下記の箇所で行われていることは、

```javascript
__decorate([
    logger
], Person.prototype, "sayHello", null);
```

要するにこれと同じということになります。

```javascript
logger(Person.prototype, "sayHello");
```
だから、第一引数の`target`にはPersonクラスのメソッドのみを持ったオブジェクト（つまりトランスパイル後の`Person.prototype`）が入り、第二引数の`key`にはメソッド名が入ってくるということですね。

ちなみに今回は省略した第三引数の`descriptor`についても、`__decorate`関数内で実行されている`Object.getOwnPropertyDescriptor(target, key)`の値がそれに当たる部分だということが分かります。

なんだ、メソッドデコレータって結局それだけか。という感じですね。  

## メソッドデコレータの評価順を解析する

デコレータを使うときは評価の順番について理解しておくことが重要ですが、`__decorate`関数の中で何が行われているかだけ把握しておけばそこまで混乱することはありません。

- メソッドデコレータを二つに増やした場合
- Decorator Factoryのパターンを使用した場合

について見ていきましょう。

### デコレータを二つに増やした場合

#### TypeScriptのソース
```typescript:index.ts
class Person {
  name: string = 'Some Name';

  @logger1
  @logger2 // これを追加
  sayHello() {
    console.log('Hello.');
  }

  sayBye() {
    console.log('Bye.');
  }
}

function logger1(target: any, key: string, desc: PropertyDescriptor) {
  console.log('This is logger1');
}

// これを追加
function logger2(target: any, key: string, desc: PropertyDescriptor) {
  console.log('This is logger2');
}
```

#### 実行結果
メソッドデコレータが二つの場合、後に書いたデコレータから評価されるようです。

```text
This is logger2
This is logger1
```

#### 解説
例によってES5へのトランスパイル後のソースを見てみましょう。
トランスパイル後は、下記のように、デコレータの関数が記述順に配列に入れられ、それが`__decorate`関数の引数となります。

```javascript:index.js
// var __decorate = ...
var Person = /** @class */ (function () {
    // function Person() {...
    __decorate([
        // ↓↓↓記述順に配列に入れられる↓↓↓
        logger1,
        logger2
    ], Person.prototype, "sayHello", null);
    return Person;
}());
// function logger1(target, key, desc) {...
// function logger2(target, key, desc) {...
```

そして、前述したように、メソッドデコレータは本質的には下記のような処理に変換されています。  

```javascript
// 再掲
var __decorate = function (decorators, target, key, desc) {
  for (var i = decorators.length - 1; i >= 0; i--) {
    decorators[i](target, key);
  }
}
```

見れば分かる通り、`decorators`配列に入っているものを後ろから実行するような処理になっているので、`logger2`、`logger1`の順に出力されたということですね。

### Decorator Factoryのパターンを使用した場合

デコレータを返す関数をDecorator Factoryと呼びます。
デコレータに引数を渡したい場合なんかに使われるのですが、このパターンを使用した場合、ややトリッキーな動きをするので注意が必要です。

#### TypeScriptのソース
```typescript:index.ts
class Person {
  name: string = 'Some Name';

  @logger1() // ()を追加
  @logger2() // ()を追加
  sayHello() {
    console.log('Hello.');
  }

  sayBye() {
    console.log('Bye.');
  }
}

// Decorator Factoryに変更
function logger1() {
  console.log('This is logger1 factory');
  return function(target: any, key: string, desc: PropertyDescriptor) {
    console.log('This is logger1');
  }
}

// Decorator Factoryに変更
function logger2() {
  console.log('This is logger2 factory');
  return function(target: any, key: string, desc: PropertyDescriptor) {
    console.log('This is logger2');
  }
}
```

#### 実行結果
評価順は、まずDecorator Factoryを上から、そしてデコレータを下から、という感じになります。

```text
This is logger1 factory
This is logger2 factory
This is logger2
This is logger1
```

#### 解説
デコレータが下から評価されるのは、前述の通りですね。
では、Decorator Factoryが上から評価されるのはなぜでしょうか？

これは、トランスパイル後のソースを見れば分かる通り、`__decorate`関数への引数として渡すときに評価されてしまうからです。

```javascript:index.js
// var __decorate = ...
var Person = /** @class */ (function () {
    // function Person() {...
    __decorate([
        logger1(), // ここでlogger1のDecorator Factoryが評価される
        logger2()  // ここでlogger2のDecorator Factoryが評価される
    ], Person.prototype, "sayHello", null);
    return Person;
}());
// function logger1() {...
// function logger2() {...
```

つまり、`__decorate`関数への引数として渡す際にまずDecorator Factoryが記述順に評価され、`__decorate`関数内でデコレータが記述の逆順に評価される、ということです。

トランスパイル後のソースを見れば、評価順に関しても、「なんだ、そういうことだったのか」という感じですね。

---

今回はメソッドデコレータに絞って解析しましたが、クラスデコレータやプロパティデコレータなども、`__decorate`関数を見れば中でどんな処理がされているかが分かるはずです。

予定をドタキャンされていきなり暇になってしまった時などに見てみてください。