こんにちは!TypeScriptの世界へようこそ。
日々の開発の中で、「クラスそのものを関数の引数として渡して、関数の中で `new`(インスタンス化)したい」と思ったことはありませんか?
例えば、共通の初期化ロジックを持つ「インスタンス生成工場(ファクトリ関数)」を作りたいときなどです。
JavaやC#などの静的型付け言語から来た方はもちろん、JavaScriptからステップアップしてきた方にとっても、ここは少しトリッキーに感じられる「壁」の一つです。
「クラスの型って、クラス名を書けばいいんじゃないの?」
「なぜかコンパイルエラーになってしまう……」
そんな疑問や悩みを抱えている方も多いはず。
今回は、TypeScriptがクラスをどう捉えているのかという本質(型システムの内側)を解き明かしながら、コンストラクタ関数を安全に引数で受け取る方法を優しく、かつ徹底的に解説します。
ここをマスターすれば、TypeScriptのオブジェクト指向プログラミングはバッチリ一歩リードですよ!
—
1. 【基本のキ】「インスタンスの型」と「クラスそのものの型」は違う
まず、一番大切な大前提から整理しましょう。
TypeScriptにおいて、「クラスから作った実体(インスタンス)」と「クラス(設計図)自体」は、全く異なる型として扱われます。
ここを混同してしまうのが、最初のつまずきポイントです。
分かりやすいように、犬のクラス `Dog` を例に考えてみましょう。
class Dog {
bark() {
return “ワンワン!”;
}
}
// 1. インスタンスを作成
const myDog: Dog = new Dog(); // この myDog の型は「Dog」です
ここで、`myDog` の型は `Dog` ですよね。これは「`Dog` クラスの設計図に従って作られた、実体(インスタンス)だよ」という意味です。
では、クラスそのものである `Dog`(`new` される前の設計図)を引数に受け取る関数を作りたい場合、その引数の型に `Dog` を指定するとどうなるでしょうか?
// ❌ 間違った実装例
function createAnimal(Creator: Dog) {
return new Creator(); // エラー発生!
}
createAnimal(Dog);
一見良さそうに見えますが、TypeScriptコンパイラはここでこう怒ってしまいます。
> `エラー: ‘new’ 表現のターゲットには、コンストラクト・シグネチャがありません。`
> `また、型 ‘Dog’ にはコンストラクト・シグネチャがありません。`
なぜでしょうか?
引数に指定した `Creator: Dog` は、「すでに出来上がった `Dog` インスタンス」を求めているからです。インスタンスに対して `new` することはできませんよね(`new myDog()` ができないのと同じです)。
私たちが渡したいのは、インスタンスではなく「`new` できる機能を持った、設計図そのもの(コンストラクタ関数)」なのです。
—
2. コンストラクタ関数を表す「new シグネチャ」の魔法
では、クラスそのもの(コンストラクタ関数)を表すには、どう型定義すればよいのでしょうか?
TypeScriptには、「これは `new` できるもの(構造体)だよ」と明示するための特別な書き方があります。それが `new` シグネチャ(Construct Signature) です。
まずは一番シンプルな書き方を見てみましょう。
// ✅ 正しい実装例:new シグネチャを使う
function createDog(Creator: new () => Dog): Dog {
return new Creator(); // これで安全に new できます!
}
const newDog = createDog(Dog);
console.log(newDog.bark()); // 出力: “ワンワン!”
この不思議な記法の意味を解剖しよう
`new () => Dog` という型定義は、一見すると奇妙に見えるかもしれません。分解して読んでみましょう。
new ( ) => Dog
─── ─── ───
| | |
| | └─ 最終的に作り出されるインスタンスの型 (Dog)
| └─ コンストラクタが受け取る引数のリスト (今回は引数なし)
└─ 「これは new キーワードをつけて呼び出せる関数だよ」という宣言
「引数なしで `new` すると、`Dog` 型のインスタンスを返すような関数(クラス)」という意味になります。
どうでしょう、こうして分解してみると、とてもシンプルで理にかなった仕様だと思いませんか?
—
3. 引数があるコンストラクタはどう定義する?
クラスのコンストラクタが引数(例えば名前など)を受け取る場合は、型定義の `()` の中にその引数をそのまま書きます。
class Cat {
name: string;
constructor(name: string) {
this.name = name;
}
meow() {
return `${this.name}「ニャー!」`;
}
}
// 引数 `name` を受け取るコンストラクタの型定義
function createCat(Creator: new (name: string) => Cat, name: string): Cat {
return new Creator(name);
}
const myCat = createCat(Cat, “タマ”);
console.log(myCat.meow()); // 出力: タマ「ニャー!」
引数の型が合わないクラスを渡そうとすると、TypeScriptがコンパイル時に「そのクラスは引数の型が違いますよ」と優しく教えてくれます。実行時のバグを未然に防げる、型安全の恩恵ですね!
—
4. ジェネリクスで「究極の万能ファクトリ関数」を作る
ここまでの知識で特定のクラスは扱えるようになりました。
しかし、実際の開発では「`Dog` でも `Cat` でも、どんなクラスでも受け取ってインスタンス化してくれる万能な関数」を作りたいことが多いはずです。
そこで登場するのが、型を引数のように扱える ジェネリクス(Generics) です。
コンストラクタの型定義とジェネリクスを組み合わせることで、「渡されたクラスの型を自動で判別し、そのインスタンス型を正確に返す関数」が作れます。
// 🌟 どんなクラスでもインスタンス化できる万能ファクトリ関数
function instantiate
return new Creator();
}
// 動作確認用のクラスたち
class Car { drive() { return “ブロロロ…”; } }
class Bike { ride() { return “キコキコ…”; } }
// 1. Car クラスを渡す
const myCar = instantiate(Car); // myCar は自動的に「Car」型になります!
console.log(myCar.drive());
// 2. Bike クラスを渡す
const myBike = instantiate(Bike); // myBike は自動的に「Bike」型になります!
console.log(myBike.ride());
なぜこれでうまくいくの?
1. `instantiate(Car)` と呼び出すと、TypeScriptは `Creator` に渡された `Car` クラスから「これは `new () => Car` だな」と推論します。
2. その結果、ジェネリクスの `T` に `Car` が当てはまります。
3. 関数の戻り値の型も `T`(つまり `Car`)になるため、呼び出し側は一切の型キャスト(`as` など)をすることなく、完璧な型安全性を手にすることができます。
—
5. 【注意!】陥りやすい2つの罠と解決策
クラスを引数に取る設計をする際、多くの開発者が一度は引っかかる代表的な罠が2つあります。あらかじめ対策を知っておきましょう。
罠①:抽象クラス(`abstract class`)は `new` できない
共通の処理をまとめるために「抽象クラス」を作ることがありますよね。
しかし、抽象クラスは直接インスタンス化(`new`)できません。
そのため、抽象クラスを `new () => T` 型の引数に渡そうとするとエラーになります。
abstract class BaseEnemy {
abstract attack(): void;
}
// ❌ 抽象クラスは new できないので、引数に渡すとコンパイルエラー!
function spawnEnemy(EnemyClass: new () => BaseEnemy) {
return new EnemyClass();
}
spawnEnemy(BaseEnemy); // エラー:抽象型を構築シグネチャに割り当てることはできません。
💡 解決策
抽象クラス自体をインスタンス化することはできません。実際に渡すのは、それを継承して具現化した「具象クラス(`class Slime extends BaseEnemy {…}` など)」にする必要があります。
もし「抽象クラスを継承したクラスなら何でも受け入れる」という制約を作りたい場合は、以下のように記述します。
class Slime extends BaseEnemy {
attack() { console.log(“体当たり!”); }
}
// BaseEnemy を継承した「具象クラス(newできるもの)」を受け取る定義
function spawnEnemy(EnemyClass: new () => BaseEnemy) {
return new EnemyClass();
}
spawnEnemy(Slime); // ✅ 具象クラスなのでOK!
罠②:インターフェース(`interface`)は渡せない
TypeScriptの `interface` は、コンパイルされてJavaScriptになると完全に消滅します(型空間にしか存在しないため)。
そのため、実体である関数に `interface` を値として渡すことはできません。
interface Animal {
sleep(): void;
}
// ❌ interface は「値」ではないので、引数として渡せません!
// instantiate(Animal); // エラー:「Animal」は値を参照していますが、ここでは型として使用されています。
引数として渡せるのは、コンパイル後もJavaScriptの「関数(コンストラクタ)」として実体が残る `class` のみであることを覚えておいてくださいね。
—
まとめ:今回の極限知見のおさらい
今回学んだ重要なポイントを振り返りましょう!
1. 二面性: クラスには「インスタンスの型」と「コンストラクタ関数の型(設計図そのものの型)」の2つの顔がある。
2. new シグネチャ: `new (…args: any[]) => T` を使うことで、「インスタンス化可能な関数」を厳密に型定義できる。
3. ジェネリクスとの融合: `new () => T` とジェネリクスを組み合わせることで、型安全で汎用的なファクトリ関数が作れる。
4. 実体の有無: `abstract class` や `interface` は直接 `new` できないため、型制約と実体の違いを意識することが大切。
コンストラクタ関数の型定義を使いこなせるようになると、デザインパターン(FactoryパターンやDependency Injectionなど)をTypeScriptで驚くほど美しく、安全に実装できるようになります。
一歩ずつ、確実に理解を深めていけば、TypeScriptはあなたの強力な相棒になってくれますよ。
これからも一緒に、楽しくコードを書いていきましょう!応援しています!