TypeScriptを使ってdecoratorの勉強をしてみた
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 は次のように最小限のものになりました。
{
"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 の中にある、 experimentalDecorators
を true
にします。(生成されたファイルはコメントアウトされているので、外します。)
{
...
"experimentalDecorators": true,
...
}
準備完了です。
実験 1: 引数のないデコレータ
babelのドキュメントにある例で実験してみます。
script.ts というファイルに、最初の例、
@annotation
class MyClass {}
function annotation(target: any) {
target.annotated = true;
}
を書いてコンパイルしてみます。
tsc -p .
とするとカレントディレクトリのプロジェクトのTypeScriptファイルがコンパイルされます。
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 を次のように書き換えました。
@isTestable(true)
class MyClass2 {}
function isTestable(value: any) {
return function decorator(target: any) {
console.log(target);
target.isTestable = value;
};
}
コンパイルします。
tsc -p .
結果のコードはこうなりました。
"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)
を付けてみました。
@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がやってくれました。
Discussion
TypeScript の experimentalDecorators フラグを使った場合は2015年時点での仕様をもとに変換されるため、今新たに学ぶ場合では適していないのではないかと思います。
@petamoriken TypeScriptのデコレータが2015年の提案で実装されていることも知りませんでしたので、とても深まりました。情報ありがとうございました。
今回は、冒頭に書きましたように、そもそもデコレータとはどういう仕組みで実現されているのか、どんなもなのかを(僕が)何も知らない状態でしたので、そもそもどんなものなのかを確認したかったというところでした。(ポッドキャストにいきさつがあります)
今後最新のTypeScriptやbabelが最近のstage 3を実装したらまたコンパイルしてみたいと思います。