TypeScriptを使ってdecoratorの勉強をしてみた

2022/04/16に公開
2

TypeScriptを用いて、極小な実験でdecoratorの勉強を(ポッドキャスト収録中に)した記録です。

Introduction

JavaScriptとかTypeScriptでよく見るデコレータは、一体何が起こっているのか、さっぱりわかっていませんでした。 @eiel に監修してもらいながら、decoratorとは何なのか勉強しました。

ちなみに、最近、decoratorの提案がtc39ででstage 2から3になったということです。

いずれにしてもdecoratorはまだ提案の段階なので、素のJavaScriptで使うことはできません。

一つはbabelを使って、どんなコードになるのか見てみる方法があります。でも「proposal-decoratorsだけ有効にしたbabelのプロジェクト」を作りたいと思いましたが、自分には一瞬ではできなさそうでした。困っていたところ、eielのアドバイスで、次のTypeScriptを使うシンプルな方法に行き着きました。

TypeScriptを使って学ぶ

そこでTypeScriptです。TypeScriptで tsc でコンパイルするといきなりJavaScriptのコードになるので、デコレータだけを有効にしたbabel設定されたプロジェクトを作るより圧倒的に簡単です。

準備

カレントディレクトリでおもむろに typescript をインストールします。

npm i typescript

すると package.json は次のように最小限のものになりました。

package.json
{
  "dependencies": {
    "typescript": "^4.6.3"
  }
}

(いつもの package.json より圧倒的に短い!)

次にカレントディレクトリにインストールをしたので tsc を実行するのが面倒かと思いきや、なんと node_modules/.bin という 相対パス にPATHを通すという方法を教えてもらいました。(今まで絶対パスしかPATHに入れたことがありませんでした)

fish で

set -x PATH $PATH node_modules/.bin

としました。(bashなどでは setenv を使います)

これで tsc コマンドが裸で使えます。

次に

tsc --init

で tsconfig.json を生成します。

tsconfig.json の中にある、 experimentalDecoratorstrue にします。(生成されたファイルはコメントアウトされているので、外します。)

tsconfig.json
{
...
  "experimentalDecorators": true,
...
}

準備完了です。

実験 1: 引数のないデコレータ

babelのドキュメントにある例で実験してみます。

script.ts というファイルに、最初の例、

script.ts
@annotation
class MyClass {}

function annotation(target: any) {
  target.annotated = true;
}

を書いてコンパイルしてみます。

tsc -p .

とするとカレントディレクトリのプロジェクトのTypeScriptファイルがコンパイルされます。

script.js ができていて中身は次のようになっていました。

script.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;
};
let MyClass = class MyClass {
};
MyClass = __decorate([
    annotation
], MyClass);
function annotation(target) {
    target.annotated = true;
}

これで MyClass にデコレータ annotation がついていたものはだいたい次のようにされることがわかりました。

  • __decorate という関数が作られる。
  • __decorate にデコレータの配列 [annotation] と、クラス MyClass が渡されて、MyClass がすげ替えられている。
  • __decorate の中身はだいたい、渡されたデコレータ1つ1つ (d) をターゲット (この例では MyClass) に対して実行し、 Object.defineProperty によってプロパティとして定義している。
  • (引数の個数 (c) によって処理を分けてある)

コンソールで実行してみます。 (注意: このJavaScriptのコードをコンソールで実行するときは、 function annotation の定義を __decorate の実行より先にやっておく必要があります。)

確かに MyClass.annotated というプロパティが生えており、その値は true でした。

まさにクラス MyClass に目印のようなデコレーションができている!

実験 2: 引数付きデコレータ

次は引数のあるデコレータの例で実験してみます。

script.ts を次のように書き換えました。

script.ts
@isTestable(true)
class MyClass2 {}

function isTestable(value: any) {
    return function decorator(target: any) {
        console.log(target);
        target.isTestable = value;
  };
}

コンパイルします。

tsc -p .

結果のコードはこうなりました。

script.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;
};
let MyClass2 = class MyClass2 {
};
MyClass2 = __decorate([
    isTestable(true)
], MyClass2);
function isTestable(value) {
    return function decorator(target) {
        console.log(target);
        target.isTestable = value;
    };
}

なるほど、引数が付いているデコレータは、引数が付いたまま __decorate 関数の引数のデコレータ配列に入れて渡されるんですね。

実験 3: 生の関数にはできない

実験として生の関数にデコレータが付けられるかやってみます。

次のように関数 hoge@isTestable(true) を付けてみました。

script.ts
@isTestable(true)
class MyClass2 {}

@isTestable(true)
function hoge(value: any) {
}

function isTestable(value: any) {
    return function decorator(target: any) {
        console.log(target);
        target.isTestable = value;
  };
}

コンパイルします。

tsc -p .

ちゃんとエラー "Decorators are not valid here." (「デコレータはここでは有効ではない。」) が出ました。

$ tsc -p .
script.ts:4:1 - error TS1206: Decorators are not valid here.

4 @isTestable(true)
  ~


Found 1 error in script.ts:4

ポッドキャストで

この様子を是非音声でお聞きください! 是非ポッドキャスト『わしポ』 エピソード 16 https://washipo.nyoho.jp/16/ の途中 0:46:34からです! この様子をカット編集してかいつまんであります。

追記

babelだとどうなるか

TypeScriptではなくbabelだとeielがやってくれました。
https://gist.github.com/eiel/887dc359320a58250165bb1e1e6960b5

GitHubで編集を提案

Discussion

NyohoNyoho

@petamoriken TypeScriptのデコレータが2015年の提案で実装されていることも知りませんでしたので、とても深まりました。情報ありがとうございました。

今回は、冒頭に書きましたように、そもそもデコレータとはどういう仕組みで実現されているのか、どんなもなのかを(僕が)何も知らない状態でしたので、そもそもどんなものなのかを確認したかったというところでした。(ポッドキャストにいきさつがあります)

今後最新のTypeScriptやbabelが最近のstage 3を実装したらまたコンパイルしてみたいと思います。