TypeScriptの型を実行時に保持するemitDecoratorMetadataと、Reflectを使わない情報取得の検証
1.TypeScriptの型とemitDecoratorMetadata
TypeScriptの型は、コンパイル時のエラーチャックや開発環境の入力補完などに利用されるだけで、実行時には何の影響もありません。実行時に何が設定されていたか確認しようとしても、トランスコンパイルされた時点で情報は綺麗さっぱり消え去ってしまいます。
TypeScriptの型を実行時に取得するには、tsconfig.jsonにemitDecoratorMetadataを設定します。この機能は実験的なものであるため、今後仕様が変更される可能性があります。ということで現時点での仕様を確認していきたいと思います。
2.TypeScriptとDecorator
TypeScriptにはJavaScriptには無い機能としてDecoratorがあります。クラスやメソッド、プロパティに対して、何らかの機能を割り込ませることが可能になります。そのDecoratorの機能の一つとして、コンパイル時にMetadataを埋め込む機能を利用することが出来ます。通常はReflectという機能を使ってMetadataを取得するのですが、今回は直接、変換されたものを取り出してみたいと思います。
3.検証用ソースコード
function dummy(...params: unknown[]) {}
var __decorate = (
param: [Function, Function],
target: Function,
key: string
) => {
console.log(`${target.constructor.name}.${key} => ${param[1].name}`);
};
var __metadata = (key: string, type: Function) => type;
class Test {
constructor() {}
@dummy
a?: number;
@dummy
b?: string;
}
__decorateと**__metadata**はクラスのプロパティに@dummyのDecoratorを挿入した時点で生成されるようになります。このプログラムはクラスの宣言しかしていませんが、以下の実行結果を得ることが出来ます。
実行結果
Test.a => Number
Test.b => String
何が起こっているか確認するためには、トランスコンパイルされたコードを見るのが手っ取り早いです。
"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 __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
function dummy() {
var params = [];
for (var _i = 0; _i < arguments.length; _i++) {
params[_i] = arguments[_i];
}
}
var __decorate = function (param, target, key) {
console.log(target.constructor.name + "." + key + " => " + param[1].name);
};
var __metadata = function (key, type) { return type; };
var Test = /** @class */ (function () {
function Test() {
}
__decorate([
dummy,
__metadata("design:type", Number)
], Test.prototype, "a", void 0);
__decorate([
dummy,
__metadata("design:type", String)
], Test.prototype, "b", void 0);
return Test;
}());
内容を確認すると__metadataに"design:type"として、型の情報が設定されています。通常だとデータはReflectに持って行かれてしまうのですが、__decorateと__metadataを再定義して、データを持ってきています。型情報が欲しいだけなら素直にReflectを使いましょう。しかし今回は動作を確認するための検証コードなのでこういう形をとっています。
4.まとめ
TypeScriptはJavaScriptに型を設定できるというところばかり注目されて、Decoratorに関してはあまり話題に上りません。QiitaにもDecoratorについて解説されている記事は何件かあるのですが、それほど流行っていない感じです。
実行時は綺麗さっぱり消え去る一般的な型定義に対して、Decoratorは実行時に力を持ちます。付加的な機能をつけたり、定義型によって挙動を変えたりと、可能性は大きく広がります。
ちなみに無理矢理情報を取得するより先に、Reflectの使い方を解説した方が先じゃないかと思った人もいるでしょう。やらない理由は簡単です。すでに該当する記事があるからです。同じような記事を書いても面白くないでしょう。
TypeScriptは便利なのですが、実際にどんなコードに変換されているのか、実はよく分からずに使っていることもあります。そういうとき、中身を見てみるのも面白いと思います。