【入門編】引数に渡す「クラスのコンストラクタ」を型定義し、DIコンテナ的な実装を行う – TypeScript コア・型システムの基礎解析バイブル

こんにちは!TypeScriptの型システムの世界へようこそ。
フロントエンドからバックエンドまで、TypeScriptであらゆるアーキテクチャを設計していく中で、「クラスのコンストラクタを引数に受けて、いい感じにインスタンスを生成・管理する仕組み(いわゆるDIコンテナのコア)」を作りたい場面に直面したことはありませんか?

「インスタンス化する前の『クラスそのもの』を引数にどう型付けすればいいの?」
「戻り値の型を、渡したクラスの型に自動で一致させたいのに、`any`まみれになってしまう……」

今回は、そんなモヤモヤを解消する「クラスのコンストラクタを型定義し、自由自在に操るジェネリクスの極意」を、優しく、そして本質的に解説していきますね。

ここをクリアすれば、TypeScriptの型推論の仕組みがグッと身近になり、設計の引き出しが何段階もレベルアップしますよ。一緒にバッチリマスターしていきましょう!

—

1. なぜ「クラスそのもの」を引数に取る必要があるのか?

私たちが普段書くコードでは、`new UserApiClient()` のように、すでに `new` されたインスタンスを別の関数やクラスに渡すことが多いですよね。

// インスタンスを渡す通常のパターン
const api = new UserApiClient();
registerService(api);

しかし、アプリケーションの規模が大きくなると、「オブジェクトの生成タイミングをコンテナ(仕組み側)に制御させたい」という要求が出てきます。つまり、関数側には `new` する前の「設計図(=クラスのコンストラクタ)」を渡し、必要なタイミングで裏側でインスタンスを生成させたいのです。

イメージ図にすると、このような関係性になります。

[ 呼び出し側 ] [ DIコンテナ(関数) ]
UserApiClient (クラスの設計図) ──► 受け取って内部で new する ──► UserApiClient のインスタンスを返す

これをTypeScriptでどう表現すればよいのでしょうか? 次のステップで見ていきましょう。

—

2. 基本のキ:クラスのコンストラクタを型として表す `new (…args: any[]) => T`

TypeScriptにおいて、クラスとは「`new` operatorをつけて呼び出すと、オブジェクトを生成する特殊な関数」に他なりません。

そのため、クラスのコンストラクタを受け取る引数の型は、次のようなシグネチャ(呼び出しシグネチャ)を使って表現します。

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

この型が何を意味しているのか、分解して解釈してみましょう。

  • `new (…)`:この型を持つものは、`new`キーワードをつけて呼び出せる(=コンストラクタである)ことを示しています。
  • `…args: any[]`:コンストラクタが受け取る引数は、何個でも、どんな型でも許容します(後で厳密にすることも可能です)。
  • `=> T`:その結果として、型 `T` のインスタンスを生成して返す、という意味になります。

この `Constructor` さえ押さえてしまえば、クラスを引数に取る関数は怖くありません!

—

3. 実践!シンプルなDI風ファクトリー関数を作ってみよう

それでは、実際に動くコードで確認してみましょう。
ここでは、受け取ったクラスのコンストラクタを元にインスタンスを生成し、綺麗に型推論してくれる `createInstance` という関数を作ってみます。

/

  • クラスのコンストラクタを表す型
  • ジェネリクス T は生成されるインスタンスの型

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

/

  • 渡されたクラスのコンストラクタを実行し、インスタンスを生成して返す関数
  • 💡 ここがポイント:
  • ジェネリクス を使うことで、引数に渡したクラスに応じた型を
  • 戻り値の型として完全に同期(推論)させています。

/
function createInstance(ctor: Constructor): T {
// 実行時に new してインスタンスを返す
return new ctor();
}

// ==========================================
// 実際の利用シーン
// ==========================================

class UserService {
public getname() {
return “Alice”;
}
}

class DatabaseConnection {
public connect() {
console.log(“Database connected!”);
}
}

// 使い方1: UserServiceクラスの設計図を渡す
// 戻り値の型は、明示しなくても自動的に `UserService` 型になります!
const userService = createInstance(UserService);
console.log(userService.getname()); // “Alice” (型安全に補完が効く)

// 使い方2: DatabaseConnectionクラスの設計図を渡す
// こちらは自動的に `DatabaseConnection` 型になる
const db = createInstance(DatabaseConnection);
db.connect(); // “Database connected!”

このコードの何が優れているのか?

もし、ここを `any` で雑に書いてしまうと、`createInstance(UserService)` の戻り値も `any` になってしまい、エディタの補完(IntelliSense)が効かなくなってバグの温床になります。

しかし、ジェネリクス `` と `Constructor` を組み合わせることで、「渡したクラスが何であれ、TypeScriptのコンパイラが自動的に戻り値の型を完璧に追跡してくれる」という、極めて堅牢な型安全性を手に入れることができるのです。

—

4. さらに応用:コンストラクタの引数(パラメータ)も型安全にする

先ほどの例では `…args: any[]` として引数を緩くしていましたが、実際のアプリケーションでは「クラスのコンストラクタに必要な引数の型」も厳密にチェックしたくなりますよね。

例えば、「初期設定として文字列のURLを受け取るサービス」を考えてみましょう。

type AbstractConstructor = new (…args: Args) => T;

class ApiClient {
// コンストラクタに endpoint の指定が必須
constructor(private endpoint: string) {}

public fetch() {
return `Fetching from ${this.endpoint}`;
}
}

/

  • コンストラクタの引数(Args)も型推論させる高度なファクトリー関数

/
function createInstanceWithArgs(
ctor: new (…args: Args) => T,
…args: Args
): T {
// 渡された引数 (args) をそのままコンストラクタにスプレッドして渡す
return new ctor(…args);
}

// ==========================================
// 使い方
// ==========================================

// ❌ エラー例: ApiClientは引数に string が必要なのに、何も渡していない
// const client = createInstanceWithArgs(ApiClient);
// 完ぺきなコンパイルエラー:Expected 1 arguments, but got 0.

// ⭕️ 正しい使い方: 第2引数以降に、コンストラクタに必要な引数を渡す
const client = createInstanceWithArgs(ApiClient, “https://api.example.com”);
console.log(client.fetch()); // “Fetching from https://api.example.com”

このように、`Args extends any[]` という制約を挟むことで、「コンストラクタに渡すべき引数の抜け漏れ」をコンパイル時に完全に検知できるようになります。これがTypeScriptの真骨頂です。

—

5. 初学者が陥りがちな文法エラーと注意点

ここで、この実装に挑戦する開発者がよくハマりがちなポイントをいくつか共有しておきますね。

1. クラスの「インスタンス」と「コンストラクタ(クラス自体)」を混同する

  • ❌ 誤り:`createInstance(new UserService())` (これだとインスタンスを渡しているので `new ctor()` の部分で `ctor is not a constructor` という実行時エラーになります)
  • ⭕️ 正しい:`createInstance(UserService)` (カッコをつけず、クラス名そのものを渡します)

2. 型定義の `new` を忘れる

  • 単なる関数型 `(…args: any[]) => T` と書くと、「関数オブジェクト」を指してしまい、`new` をつけて呼び出すことができません。クラスのコンストラクタを表現する場合は必ず頭に `new` をつけましょう。

—

まとめ

いかがだったでしょうか?
今回は、クラスのコンストラクタを型として捉え、ジェネリクスを駆使して堅牢なDI的な仕組みの土台を作る方法を解説しました。

  • クラスのコンストラクタ型は `new (…args: any[]) => T` で表現できる
  • ジェネリクスを使うことで、渡したクラスと戻り値の型を完全に同期(推論)させられる
  • 引数の型も `Args extends any[]` で縛ることで、インスタンス化のミスをコンパイル時に防げる

このテクニックは、小規模なスクリプトから、NestJSのような本格的なフレームワークの内部実装まで、あらゆる場所で応用できる普遍的な知見です。

「クラスもただの値(ファーストクラスオブジェクト)であり、型として自在にコントロールできるんだ」という感覚を掴めれば、あなたのTypeScriptスキルは次のステージへ確実に到達していますよ。

ぜひ今日のコードをご自身の環境でも試して、型推論の心地よさを体感してみてくださいね!

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