1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-07-30

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

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

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

デコレータそのものについては説明していないので、「そもそもデコレータとは?」という方には下記の記事をおすすめします。
TypeScriptによるデコレータの基礎と実践

ClassからPrototypeへの変換を見てみる

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

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

TypeScriptのソース

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

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

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

トランスパイル後の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を通じてsayHellosayByeを実行できるような作りになっています。

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

  • nameはPersonオブジェクトのプロパティであるが、
  • sayHellosayByeはPersonオブジェクトのメソッドではなく、Person.prototypeオブジェクトのメソッドである

ということです。

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

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

TypeScriptのソース

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

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(今回は省略)を受け取ります。

実行結果

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

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

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

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

解説

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

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関数自体は、ごちゃごちゃといろいろ書いてありますが、今回の理解に必要な部分だけ抜き出すと、

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

こんな感じになります。

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

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

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

logger(Person.prototype, "sayHello");

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

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

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

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

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

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

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

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

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

実行結果

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

This is logger2
This is logger1

解説

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

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) {...

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

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

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

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

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

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を上から、そしてデコレータを下から、という感じになります。

This is logger1 factory
This is logger2 factory
This is logger2
This is logger1

解説

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

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

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関数を見れば中でどんな処理がされているかが分かるはずです。

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

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?