TypeScriptのInterfaceでStaticなクラスメソッドを適用させる方法


2022/10/13
【javascript応用編】中断もできてasync/awaitも使える再帰setTimeoutループ処理のPromise版の自作する
蛸壺の技術ブログ|TypeScriptのInterfaceでStaticなクラスメソッドを適用させる方法

以前の当ブログのお題目で、「TypescriptのInterfaceからasync関数(Promise)を利用する」ときの注意点を解説したときがありました。

合同会社タコスキングダム|蛸壺の技術ブログ
TypescriptのInterfaceでasync関数を定義する・async関数でクラス変数(this)を使う

Typescriptでinterfaceやclassからasync/await関数を付けて使いたいときの注意点を考えます。

typescriptのInterfaceは「ポリモーフィズム」や「ダックタイピング」に欠かせない重要な仕組みですが、元々のjavascriptとの兼ね合いもあり、
「interfaceからstaticなメンバーフィールド・関数は指定できない」というのが現状で原則となっています。

しかし、interfaceから実装クラスにstaticな関数が指定できないと、たまに困るときがあります。

今回は、どうしてもinterfaceからStatic関数を特定のクラスに実装したい方向けにちょっとニッチなテクニックを紹介してみます。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】Javascript&Typescriptプログラミング入門のためのオススメ書籍&教材特集

2つのinterfaceに分離して、別々に適用する方法

まずは基礎的な手法の手っ取り早く解決できるテクニックをやってみましょう。

2つのinterfaceを用意し、以下のソースコードでユニットテストしてみましょう。

            
            //👇通常のimplementでclassに実装させるinterface
interface IInstance {
    instanceId: number;
    instanceName: string;
    instanceMethod(): void;
}

//👇Staticなメソッドと自己インスタンス化(new)を担当するコンストラクタの代役的interface
interface IStatic {
    new(instanceId: number, instanceName: string): IInstance;
    staticMethod(msg: string): void;
}

const HogeClass: IStatic = class HogeClass implements IInstance {
    instanceId: number;
    instanceName: string;
    constructor(instanceId: number, instanceName: string){
        this.instanceId = instanceId;
        this.instanceName = instanceName;
    }

    instanceMethod(){
        console.log(`Hello ${this.instanceName} of ID#${this.instanceId} Who Called from Instance Method in TS Class!`);
    }

    static staticMethod(msg: string) {
        console.log(msg);
    }
};

//👇通常のクラスメソッドの呼び出し
(new HogeClass(1, 'Hoge')).instanceMethod();

//👇スタティッククラスも利用可
HogeClass.staticMethod('Hello Somebody Who Called from Static Method in TS Class!');
        
実行結果は、

            
            Hello Hoge of ID#1 Who Called from Instance Method in TS Class!
Hello Somebody Who Called from Static Method in TS Class!
        
という感じになります。

このやり方では、普通のInterfaceで利用(implements)できるものと、そうでないもの(staticなフィールド)を2つに別々のinterfaceとして分離するのがポイントです。

こうすることで、new演算子が返すクラスインスタンスも分離することができます。

new演算子(関数)は普段は意識することがないですが、暗黙的にクラスのコンストラクタを呼び出して、クラスをインスタンス化した
thisを返す仕組みになっています。

後者のStaticなフィールドを含むinterfaceの中で、
new(...): IInstanceとnewをオーバーロードして、コンストラクタで返すインスタンス先を変えることで、クラス内に定義したStaticな関数を呼び出す、ということを実現しています。

理屈が分かればどうということはないのですが、「interfaceをインスタンス化するものとしないものの2つに分ける」という作業が発生してしまうので、知らない人が見ると不可解なコードに見えてしまう恐れがあり、無用に混乱させてしまうかもしれません。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】Javascript&Typescriptプログラミング入門のためのオススメ書籍&教材特集

クラスデコレータでコンストラクタ関数を置換する方法

先ほどと理屈の上では同じですが、「クラスデコレータ」を利用すると、もう少しスマートに書けるようになります。

ただし、typescriptでのデコレータ機能自体はまだ試験的なものですので、利用する場合には
tsconfig.jsonを以下のように変更します。

            
            {
    //...中略
    "compilerOptions": {
        //...中略
        "experimentalDecorators": true
    }
}
        
デコレータが利用できるようになったら、以下のソースコードでユニットテストしてみましょう。

            
            //👇クラスデコレータの実装
function staticImplements<T>() {
    return <U extends T>(constructor: U) => {constructor};
}

interface IStatic {
    staticMethod(msg: string): void;
}

@staticImplements<IStatic>()
class HogeClass {
    public static staticMethod(msg: string) {
        console.log(msg);
    }
}

HogeClass.staticMethod('Hello, Static Method in TS class!');
        
これを実行すると、

            
            Hello, Static Method in TS class!
        
となっていればOKです。

ここではいわゆる
implements演算子の更に高機能版として自作の@staticImplementsを作っています。

詳しくその用法自体に触れませんが、クラスデコレータはクラスのコンストラクタに適用され、prototype等のクラス定義の書き換える際に使用することができます。

さらに、
クラスデコレータが値を返す場合は、デコレータ指定されたクラスのコンストラクタ関数を置き換えるルールが適用されます。

この性質を上手く使って、先程説明していたような
「コンストラクタ関数の入れ替え」をエレガントに行うことが可能になっています。

早い話が、このデコレータをクラス宣言位置に修飾してあげると、
new HogeClass(...)が糖衣構文化されて、HogeClassのクラス名だけで上手くインスタンス化することができるわけです。

コンストラクタの書き換えなんて、なんだか狐につままれるようなテクニックですが、クラスデコレータを知っていると他にも様々なクラスの機能拡張に便利に使うことができるかも知れません。

それだけ、デコレータは強力な機能だということを意識されていると良いでしょう。めでたしめでたし。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】Javascript&Typescriptプログラミング入門のためのオススメ書籍&教材特集

参考サイト

TypeScript: classに定義したstaticメソッドをinterfaceで型定義するときのMEMO

How to define static property in TypeScript interface

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。

合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】Javascript&Typescriptプログラミング入門のためのオススメ書籍&教材特集