TypeScriptを書いているとクラス名の文字列でクラスを取得したくなる事がある。JavaのClass.forClass("ClassName")
やC#のType.GetType("ClassName")
に相当することをするにはどうしたらいいのだろうか。
特定状況下なら有効な方法
特定状況下なら凄く簡単な方法が一つある。
var cls:typeof 【クラス名】 = window["【クラス名】"];
問題はこれはどこでも使えるわけではないということ。
- ブラウザでしか動かない
-
windows
オブジェクトがグローバルオブジェクトとして存在する環境でしか動かないのでnode.jsなどでは使えない
- ES3/ES5に変換した時のみ参照可能
- グローバルなスコープで定義した時のみ
-
namespace
でスコープを区切るとつかえない。
使える条件が限られるので正直あまり実用的でない。
文字列からクラスを取得するユーティリティ関数ClassUtil.getClass
ということで作ってみたのが以下。無名クラスやスコープの深いクラス、Date
などのネイティブなグローバルオブジェクトに対応させたので少し長い。
"use strict";
namespace ClassUtil {
export interface ClassType<T> {
prototype?: T;
name?: string;
[property: string]: any;
new (...args:any[]): T;
}
}
export class ClassUtil {
static getClass<T>(className: string): ClassUtil.ClassType<T> {
if (!className) {
throw Error(`Cannot get a anonymous class. A class you want to get must be named.`);
}
const g = this.getGlobal();
//get scope reference
const p = className.split(".");
let c = g;
if(p.length >= 0){
for (let i of p) {
if (typeof c[i] === "undefined") {
if(typeof (<any>global)[i] === "undefined"){
throw ReferenceError(`No such a reachable reference scope "${i}". `);
}else{
//global objects
c = (<any>global)[i];
}
}else{
c = c[i];
}
}
}
if (this.isClassLike<T>(c, className)) {
return <typeof c>c;
} else {
throw TypeError(`${className} is not a Class.`)
}
}
/**
* @return the global object or node.js exports
*/
private static getGlobal(): any {
if (typeof self !== "undefined") { return self; }
else if (typeof window !== "undefined") { return window; }
else if (typeof global !== "undefined" && typeof exports !== 'undefined') { return exports; }
else { throw new Error(`Cannot get the global object or exports object (nodejs).`); }
}
private static isClassLike<T>(classLike: any, className: string): classLike is T {
const cnl = className.split(".");
return (typeof classLike === "function")
&& (typeof classLike.prototype !== "undefined")
&& (classLike.name === cnl[cnl.length - 1]);
}
}
使い方
呼び出したいクラスはexport
で修飾し1、-m commonjs
としておく2。
class TestBase{constructor(){return Object.getPrototypeOf(this).name}}
export class Test1 extends TestBase{}
export class Test2 extends TestBase{}
export class Test3 extends TestBase{}
Test1,Test2,Test3
からランダムに選んでインスタンスを生成する例を考えてみる。
```ts:Test1,Test2,Test3
からランダムに選んでインスタンスを生成する例
type TestN = Test1|Test2|Test3;
const cName = `Test${1+Math.round(Math.random()*2)}`;
var tx = ClassUtil.getClass(cName);
var itx = new tx();
console.log(`itx:${tx.name}`); //Test1 or Test2 or Test3
`ClassUtil.getClass`が返すのはクラスのインスタンスではなく、**クラスそのもの**だ。なので `getClass<T>()=>T` ではなく、`getClass<T>()=>ClassType<T>`と一旦`ClassType<T>`インターフェイスを経由[^3]し、インスタンス生成時に`T`の型になるよう型情報を引き継いでいる。
今回のように、選択肢が事前に決まっている場合は、TypeAliasと組み合わせる事でnewしたあとのインスタンスが`Test1/Test2/Test3`のどれかになる、というのが実現できる。
## ネームスペース付きクラスを取得
ネームスペースが区切られた深いところにいるクラスの取得にも対応している。
```ts
export namespace ns {
export class InnerClass {
prop = "inner";
}
}
namespace, classともにexportが必要。
//namespaced class test
var it = ClassUtil.getClass<ns.InnerClass>("ns.InnerClass");
var iit = new it();
console.log(`iit.prop:${iit.prop}`); //iit.prop:inner
グローバルオブジェクトを取得
クラスではないが、一部のグローバルオブジェクト(Date
とかNumber
とか)の文字列による取得にも対応している。
var date = ClassUtil.getClass<Date>("Date");
var idata = new date();
console.log(`idata:${idata.getFullYear()}`);
対応するオブジェクト:
-
typeof 【オブジェクト名】 === "function"
なオブジェクト - グローバルなスコープで定義されている
-
prototype
,name
プロパティを持つ
これは副産物で、クラスであるかどうか厳密には判定できないが故の特別対応。
制限
匿名クラス
クラス式で表現されるような匿名クラス(無名クラス)には名前がない。そのため当然ながら対応していない。
export var cls = class {
constructor(){return `this is anonymous class!`}
}
//get anonymous class with exported var
var ac = ClassUtil.getClass("cls");
var iac = new ac(); //TypeError: cls is not a Class.
取得したオブジェクトを型クエリで型指定
export class Test{
static hoge() { return 0 };
}
var t: typeof Test = ClassUtil.getClass<Test>("Test"); //Property 'hoge' is missing in type 'ClassType<Test>'.
取得したクラスそのものは、TypeScriptだと型クエリで型指定できるのだが、今回は途中にインターフェイスを経由する関係で、エラーになることがある。具体的には呼び出そうとするクラスがstatic
なメソッドを持つ場合。
getClass<T>()=> typeof T
のように関数の返り値に型パラメータと型クエリを組み合わせられればこのような制限はなかったのかもしれないが、現状のTypeScriptだと表現不可。
まとめ
- TypeScript(JavaScriptも)で文字列からクラスを取得することはできるけど厄介
- 型クエリをのぞけば上記のやり方で取得できる…はず
- もし、クラスそのものを返す関数に
func<T>() => typeof T
って表現できるようになるとメタプロが捗る