TypeScriptの型は幻ではない! ~ 型宣言を実行中のチェックに使う ~
幻を現実とする
TypeScriptでは静的な型付けが可能です。しかしいくら型を宣言しようとも、簡単に内容を誤魔化すことができてしまいます。厳密に型チェックを行うのであれば、コンパイル時だけでなく、実行時にも型をチェックしなければなりません。もちろん手動で一つ一つ書いていけば良いのですが、それでは面倒です。
今回はできるだけ簡単にクラスのメソッドに実行時の型チェック機能を付けるという内容をやっていきます。
##事前準備
TypeScriptの設定のうち、デコレータとメタデータを有効にします。これによって型情報がコンパイル時の出力ファイルに埋め込まれるようになります。
{
"compilerOptions": {
"experimentalDecorators": true
"emitDecoratorMetadata": true
}
}
ソースコード
型チェック機能の実装部分です。デコレータでメソッドに割り込みをかけて、メタデータの型情報と照合します。
import "reflect-metadata"; //npmから追加インストールの必要あり
//型のチェック
function isType(type: object, value: unknown) {
switch (type) {
case Number:
if (typeof value !== "number") return false;
break;
case String:
if (typeof value !== "string") return false;
break;
case Boolean:
if (typeof value !== "boolean") return false;
break;
case Array:
if (!(value instanceof Array)) return false;
break;
case Function:
if (!(value instanceof Function)) return false;
break;
}
return true;
}
//型チェック用デコレータ
function CHECK(target: any, name: string, descriptor: PropertyDescriptor) {
const ptypes = Reflect.getMetadata(
"design:paramtypes",
target,
name
) as object[];
const rtype = Reflect.getMetadata(
"design:returntype",
target,
name
) as object[];
return {
...descriptor,
value: function(...params: unknown[]) {
if (ptypes.length !== params.length) throw "引数の数が不正";
const flag = ptypes.reduce((a, b, index) => {
return a && isType(b, params[index]);
}, true);
if (!flag) {
throw "引数の型が不正";
}
const result = descriptor.value.apply(this, params);
if (!isType(rtype, result)) throw "戻り値の型が不正";
return result;
}
};
}
サンプル用にクラスを作り、デコレータを設定したメソッドを用意します
//テスト用クラス
class Test {
@CHECK //これを付けると実行時に引数と戻り値の型がチェックされる
func01(a: number, b: string, c: boolean): number {
console.log(a, b, c);
return 0;
}
@CHECK
func02(a: number, b: string, c: boolean): number {
console.log(a, b, c);
return "A" as never; //戻り値をstring型にする
}
}
実際に動かすと、引数や戻り値のチェックができていることが分かります。
//インスタンスの作成
const test = new Test();
//真っ当に実行
test.func01(0, "A", true); //OK
//引数の数を間違える
try {
(test.func01 as any)(true); //例外 "引数の数が不正"
} catch (e) {
console.error(e);
}
//引数の型を間違える
try {
(test.func01 as any)(0, 10, true); //例外 "引数の型が不正"
} catch (e) {
console.error(e);
}
//戻り値が間違ったメソッドを呼び出す
try {
test.func02(0, "A", true); //例外 "戻り値の型が不正"
} catch (e) {
console.error(e);
}
チェックの限界
単純な型しかチェックできません。複合的な型のチェックも出来ません。複合的な型は作ったとたんにメタデータがObject型になってしまうので判定しようがないのです。ということでObject型の判定はあえてやっていません。これ以上の領域に進むならCompilerAPIを使って、元ソースやd.tsから情報を引っ張ってくるという愉快な作業をすることになります。
まとめ
TypeScriptでは使いどころを見つけるのが難しいデコレータと、ますます使い道が難儀なメタデータを取り扱ってみました。もともとこの機能はExperimental Optionsとなっているので、もしかしたらもっと高機能な何かが実装される日が来るのかもしれません。