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

こんにちは。TypeScriptの世界へようこそ。

現場でコードを書いていると、「DI(依存性の注入)コンテナ」という言葉を耳にする機会が増えますよね。でも、いざ自分で実装しようとすると、`new` をどうやって型安全に扱うかでつまずく人が本当に多いんです。

今日は、「クラスのコンストラクタを型として受け取り、安全にインスタンス化する」という、TypeScriptの中級者への登竜門を一緒に突破しましょう。ここを理解すれば、あなたの書くコードは「ただ動くだけのもの」から「堅牢で拡張性の高い設計」へと進化します。

—

なぜ「クラスのコンストラクタ」を型として定義するのか?

TypeScriptにおいて、クラスは「型」であると同時に「値」でもあります。
`class User {}` と書くと、`User` という型と、`new User()` で呼び出せるコンストラクタ関数の両方が生成されます。

もし、あなたが「どんなクラスでも受け取って、それをインスタンス化する関数」を作りたいとき、ただの `any` を使ってしまうと、TypeScriptの恩恵をすべて捨ててしまうことになります。

まずは、正しい型定義の方法を見ていきましょう。

「newできること」を型にする

TypeScriptには、コンストラクタ型を表現するための特別な構文があります。

// T という型のインスタンスを生成するコンストラクタの型
type Constructor = new (…args: any[]) => T;

// 例:
class Logger {
log(msg: string) { console.log(msg); }
}

// この関数は「Loggerクラス」を引数として受け取れる
function createInstance(ctor: Constructor): T {
return new ctor(); // ここで安全に new できる!
}

const myLogger = createInstance(Logger);
myLogger.log(“Hello, DI!”); // 型推論が効いて、logメソッドが補完される

この `new (…args: any[]) => T` という構文、少し不思議に見えますよね?
これは、「任意の引数を受け取り、T型のインスタンスを返す関数(=コンストラクタ)」という宣言なんです。

—

現場で役立つ!DIコンテナの小さな実装

さて、ここからが本題です。コンストラクタに引数が必要な場合、どうすればいいでしょうか?
例えば、サービスが依存する「データベース接続」などをコンストラクタ経由で注入したい場合です。

依存注入を考慮したファクトリの実装

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

class Database {
query = () => “SELECT FROM users”;
}

class UserService {
constructor(private db: Database) {}
execute() { return this.db.query(); }
}

// DIコンテナの簡易版
class Container {
// コンストラクタと依存解決を管理
resolve(ctor: Constructor): T {
// 実際には依存関係を自動解決するロジックが入る
const db = new Database();
return new ctor(db);
}
}

const container = new Container();
const userService = container.resolve(UserService);
console.log(userService.execute()); // “SELECT FROM users”

この仕組みの素晴らしい点は、`resolve` メソッドの戻り値が `T` であるため、受け取り側がわざわざ `as UserService` などと型キャストする必要がないことです。TypeScriptが「このコンストラクタを渡せば、UserServiceのインスタンスが返ってくるはずだ」と完璧に理解してくれるからです。

—

陥りやすい罠:なぜ「クラス型」ではダメなのか?

よくある間違いが、`Constructor` 型の代わりに `typeof ClassName` を使おうとすることです。

// 悪い例
function create(ctor: typeof Logger) {
return new ctor();
}

これでも動きますが、「Loggerクラス専用」の関数になってしまい、再利用性がゼロになります。
DIコンテナを作るなら、`Constructor` のようにジェネリクスを使うのが鉄則です。そうすることで、どんなクラスでも注入可能な「汎用的なコンテナ」に生まれ変わるからです。

また、`new` を伴わない普通の関数を渡してしまった場合、TypeScriptはコンパイル時にエラーを出して止めてくれます。これが「型安全」の本当の価値です。実行時ではなく、書いている瞬間にミスに気づける。最高ですよね。

—

最後に:型をマスターすることは、設計をマスターすること

「クラスを引数にする」というテクニックは、一見すると魔法のように感じるかもしれません。しかし、これはTypeScriptがコンパイル時にクラスという「設計図」をどう扱っているかを理解すれば、非常に論理的なアプローチであることが分かります。

1. `new (…args: any[]) => T` で「インスタンス生成能力」を型として定義する。
2. ジェネリクスでクラスを抽象化する。
3. DIコンテナで依存関係を外部から注入する。

この3ステップが理解できれば、大規模なアプリケーションの設計も怖くありません。

もしコードがエラーを吐いたら、それは「TypeScriptがあなたの設計の矛盾を事前に教えてくれている」ということです。エラーメッセージを敵に回さず、対話してみてください。そうすれば、あなたはもうTypeScriptの達人への道を歩んでいますよ。

ここをクリアできれば、フロントエンドのコンポーネント設計からバックエンドのアーキテクチャまで、応用範囲は無限大です。ぜひ、自分のプロジェクトで試してみてくださいね!

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