文字列からクラスを取得する

TypeScriptを書いているとクラス名の文字列でクラスを取得したくなる事がある。Javaの`Class.forClass("ClassName")`やC#の`Type.GetType("ClassName")`に相当することをするにはどうしたらいいのだろうか。

## 特定状況下なら有効な方法

**特定状況下なら**凄く簡単な方法が一つある。

```ts
var cls:typeof 【クラス名】 = window["【クラス名】"];
```

問題はこれは**どこでも使えるわけではない**ということ。

 1. ブラウザでしか動かない
  * `windows`オブジェクトがグローバルオブジェクトとして存在する環境でしか動かないのでnode.jsなどでは使えない
 2. ES3/ES5に変換した時のみ参照可能
  * 理由：[ES6(ES2015)でグローバルに定義したlet,const,classはグローバルオブジェクトのプロパティにならない](http://qiita.com/ConquestArrow/items/8cb01744af15afff33c0) 
 3. グローバルなスコープで定義した時のみ
  * `namespace`でスコープを区切るとつかえない。

使える条件が限られるので正直あまり実用的でない。

## 文字列からクラスを取得するユーティリティ関数`ClassUtil.getClass`

ということで作ってみたのが以下。無名クラスやスコープの深いクラス、`Date`などのネイティブなグローバルオブジェクトに対応させたので少し長い。

```ts:文字列からクラスを取得するユーティリティ関数
"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]。

```ts:文字列から呼び出したいクラスの定義
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<TestN>(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が必要。

```ts
//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`とか）の文字列による取得にも対応している。

```ts:ClassLikeなグローバルオブジェクトを取得
var date = ClassUtil.getClass<Date>("Date");
var idata = new date();
console.log(`idata:${idata.getFullYear()}`);
```

対応するオブジェクト：

 - `typeof 【オブジェクト名】 === "function"`なオブジェクト
 - グローバルなスコープで定義されている
 - `prototype`,`name`プロパティを持つ

これは副産物で、クラスであるかどうか厳密には判定できないが故の特別対応。

## 制限

### 匿名クラス

クラス式で表現されるような匿名クラス（無名クラス）には名前がない。そのため当然ながら対応していない。

```ts:匿名クラスには対応しない
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.
```


### 取得したオブジェクトを型クエリで型指定

```ts:staticなメソッドを持つクラスの場合、型クエリで型指定不可
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も)で文字列からクラスを取得することはできるけど厄介
 * 型クエリをのぞけば[上記のやり方](#文字列からクラスを取得するユーティリティ関数`ClassUtil.getClass`)で取得できる…はず
 * もし、クラスそのものを返す関数に`func<T>() => typeof T`って表現できるようになるとメタプロが捗る



[^1]: exportしておかないと外部からアクセスできないため。文字列で取得するのも外部から参照しているのと同じ扱いになる。
[^2]: 現時点のnodejs環境を考えるとCommonJSに変換しておくのが無難なため。
[^3]: この欠点は`:typeof 【クラス名】`のように型クエリで指定できないこと。