こんにちは。TypeScriptの世界へようこそ。
現場でコードを書いていると、「DI(依存性の注入)コンテナ」という言葉を耳にする機会が増えますよね。でも、いざ自分で実装しようとすると、`new` をどうやって型安全に扱うかでつまずく人が本当に多いんです。
今日は、「クラスのコンストラクタを型として受け取り、安全にインスタンス化する」という、TypeScriptの中級者への登竜門を一緒に突破しましょう。ここを理解すれば、あなたの書くコードは「ただ動くだけのもの」から「堅牢で拡張性の高い設計」へと進化します。
—
なぜ「クラスのコンストラクタ」を型として定義するのか?
TypeScriptにおいて、クラスは「型」であると同時に「値」でもあります。
`class User {}` と書くと、`User` という型と、`new User()` で呼び出せるコンストラクタ関数の両方が生成されます。
もし、あなたが「どんなクラスでも受け取って、それをインスタンス化する関数」を作りたいとき、ただの `any` を使ってしまうと、TypeScriptの恩恵をすべて捨ててしまうことになります。
まずは、正しい型定義の方法を見ていきましょう。
「newできること」を型にする
TypeScriptには、コンストラクタ型を表現するための特別な構文があります。
// T という型のインスタンスを生成するコンストラクタの型
type Constructor
// 例:
class Logger {
log(msg: string) { console.log(msg); }
}
// この関数は「Loggerクラス」を引数として受け取れる
function createInstance
return new ctor(); // ここで安全に new できる!
}
const myLogger = createInstance(Logger);
myLogger.log(“Hello, DI!”); // 型推論が効いて、logメソッドが補完される
この `new (…args: any[]) => T` という構文、少し不思議に見えますよね?
これは、「任意の引数を受け取り、T型のインスタンスを返す関数(=コンストラクタ)」という宣言なんです。
—
現場で役立つ!DIコンテナの小さな実装
さて、ここからが本題です。コンストラクタに引数が必要な場合、どうすればいいでしょうか?
例えば、サービスが依存する「データベース接続」などをコンストラクタ経由で注入したい場合です。
依存注入を考慮したファクトリの実装
type Constructor
class Database {
query = () => “SELECT FROM users”;
}
class UserService {
constructor(private db: Database) {}
execute() { return this.db.query(); }
}
// DIコンテナの簡易版
class Container {
// コンストラクタと依存解決を管理
resolve
// 実際には依存関係を自動解決するロジックが入る
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の達人への道を歩んでいますよ。
ここをクリアできれば、フロントエンドのコンポーネント設計からバックエンドのアーキテクチャまで、応用範囲は無限大です。ぜひ、自分のプロジェクトで試してみてくださいね!