Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
15
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

@ConquestArrow

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

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

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

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

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

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

  1. ブラウザでしか動かない
    • windowsオブジェクトがグローバルオブジェクトとして存在する環境でしか動かないのでnode.jsなどでは使えない
  2. ES3/ES5に変換した時のみ参照可能
  3. グローバルなスコープで定義した時のみ
    • 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からランダムに選んでインスタンスを生成する例を考えてみる。

`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のどれかになる、というのが実現できる。

ネームスペース付きクラスを取得

ネームスペースが区切られた深いところにいるクラスの取得にも対応している。

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

ClassLikeなグローバルオブジェクトを取得
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.

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

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も)で文字列からクラスを取得することはできるけど厄介
  • 型クエリをのぞけば上記のやり方で取得できる…はず
  • もし、クラスそのものを返す関数にfunc<T>() => typeof Tって表現できるようになるとメタプロが捗る

  1. exportしておかないと外部からアクセスできないため。文字列で取得するのも外部から参照しているのと同じ扱いになる。 

  2. 現時点のnodejs環境を考えるとCommonJSに変換しておくのが無難なため。 

  3. この欠点は:typeof 【クラス名】のように型クエリで指定できないこと。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
15
Help us understand the problem. What are the problem?