【実務・中級編】引数に渡す「クラスのコンストラクタ」を型定義し、DIコンテナを安全に実装する – TypeScript コア・型システムの基礎解析バイブル

コンストラクタ型を掌握せよ:DIコンテナを「型安全」に構築する高度な設計術

TypeScriptの型システムを使いこなす上で、避けて通れない領域がある。それが「クラスのコンストラクタを値として受け取り、型安全にインスタンス化する」というファクトリ設計だ。

多くのエンジニアが `any` で型を濁したり、単なる `Function` 型で妥協したりしている。だが、それはコンパイル時の静的解析を放棄し、実行時のランタイムエラーを招く敗北に他ならない。本稿では、TypeScriptのコンパイラAPIの知見を元に、堅牢なDI(依存性の注入)コンテナの核となる「インスタンス化ファクトリ」の極意を伝授する。

—

1. コンストラクタ型の正体:`new` シグネチャの真髄

TypeScriptにおいて、クラスは「そのクラスのインスタンスを生成する能力を持つ関数」である。これを型として表現するには `new` シグネチャを使用する。

// コンストラクタ型定義の基本
type Constructor = new (…args: any[]) => T;

ここで重要なのは、`new` を含むシグネチャを定義することで、TypeScriptコンパイラに対して「この変数は `new` 演算子を適用可能である」という制約を強制できる点だ。

—

2. 実践:型安全なDIコンテナの設計

DIコンテナの要諦は「具象クラスを知らずとも、インスタンスを生成・管理できること」にある。引数の型を推論させつつ、戻り値を特定のインターフェースに縛るのがプロの仕事だ。

汎用的なインスタンス化ファクトリ

以下のコードは、特定の基底クラスを継承したクラスのみを許可し、かつ引数の型まで正確に推論させる実装例である。

/

  • DIコンテナの核となるファクトリ関数
  • @param ctor 生成対象のクラス(コンストラクタ)
  • @param args コンストラクタに渡す引数

/
function createInstance(
ctor: new (…args: Args) => T,
…args: Args
): T {
// コンパイル時: argsの型はctorの引数と完全に同期される
// 万が一、引数が足りなければ型エラーが発生する
return new ctor(…args);
}

// — 利用例 —
class Logger {
constructor(private prefix: string) {}
log(msg: string) { console.log(`[${this.prefix}] ${msg}`); }
}

// 正しい呼び出し
const logger = createInstance(Logger, “DEBUG”);

// エラー: 必要な引数が足りない場合はコンパイルエラーになる
// const invalid = createInstance(Logger);

なぜこれが「強力」なのか

この実装では、`Args` というジェネリクス型変数を使って、コンストラクタの引数を `tuple` としてキャプチャしている。これにより、IDEのインテリセンスはコンストラクタの引数を動的に解決し、開発者は「何を渡すべきか」をコンパイル前に知ることができる。

—

3. 実務での応用:抽象と具象の分離

大規模なフロントエンドアプリケーションでは、サービス層のインスタンスをシングルトンとして管理したいケースが多い。ここで `Constructor` 型を活用する。

type ServiceConstructor = new (…args: any[]) => T;

class Container {
private instances = new Map, any>();

register(ctor: ServiceConstructor, instance: T) {
this.instances.set(ctor, instance);
}

resolve(ctor: ServiceConstructor): T {
const instance = this.instances.get(ctor);
if (!instance) throw new Error(`${ctor.name} is not registered`);
return instance;
}
}

このパターンは、テスト時に「モッククラス」を差し替える際にも非常に有効だ。コンストラクタを型として扱うことで、依存関係の解決をランタイムの文字列探索ではなく、型安全なルックアップへと昇華させている。

—

4. 陥りやすい罠とパフォーマンスの注意点

インスタンス化のコスト

`new ctor(…)` はJavaScriptエンジンにとって最適化が効きにくいコードパスになる可能性がある。ホットループ(高頻度で呼ばれる場所)でDIコンテナを多用するのは避けるべきだ。DIはあくまでアプリケーションのライフサイクル初期の「配線」にのみ使うのが賢明である。

`any[]` の扱い

`…args: any[]` を使用しているが、これはジェネリクス `Args extends any[]` で制約をかけているため、外側からは型安全性が保証されている。むやみに `any` を型定義に晒さず、ジェネリクスで包み込むのが、TypeScriptにおける「カプセル化」の流儀である。

—

結論:型は「ドキュメント」以上の存在

TypeScriptの型システムは、単なるエラーチェック機能ではない。それはプログラムの「設計図」そのものだ。`new` シグネチャを活用し、コンストラクタをファーストクラスの値として扱うことで、あなたのコードは劇的に堅牢になる。

次にコードレビューをする際は、`any` を見つけたら「なぜジェネリクスで解決しなかったのか?」と問いかけてほしい。型を制する者が、アーキテクチャを制するのだ。

さあ、あなたのDIコンテナを次のレベルへ押し上げよう。

タイトルとURLをコピーしました