ゴール:「“クラスそのもの”から“インスタンスの型”だけをきれいに取り出す」感覚をつかむ
ここでのテーマは、
「クラス(コンストラクタ)から、そのインスタンスの型だけを取り出す」
です。
TypeScript には、まさにそのためのユーティリティ型InstanceType<T> が用意されています。
これを理解すると、
typeof クラス(クラス側の型)InstanceType<typeof クラス>(インスタンス側の型)
をきれいに使い分けられるようになります。
前提整理:クラスの「クラス側」と「インスタンス側」
クラス宣言から見える2つの世界
まずはシンプルなクラスから。
class User {
constructor(public name: string, public age: number) {}
greet(): void {
console.log(`こんにちは、${this.name}です`);
}
}
TypeScriptここには、2つの“世界”があります。
1つ目は「インスタンス側」。
const u = new User("Taro", 20);
function printUser(user: User) {
console.log(user.name, user.age);
user.greet();
}
TypeScriptここでの User は、
「new User(...) で作られるオブジェクトの型」=インスタンス型
として使われています。
2つ目は「クラス側(コンストラクタ側)」。
const C = User; // クラスそのものを代入
const u2 = new C("Hanako", 30);
TypeScriptここでの User は「値」としてのクラス(コンストラクタ)です。
この「クラス側の型」を取りたいときは typeof User を使います。
typeof クラス と InstanceType の関係
typeof クラス は「クラスそのものの型」
さっきの User を使って、型を見てみます。
class User {
constructor(public name: string, public age: number) {}
static fromName(name: string): User {
return new User(name, 0);
}
}
type UserClass = typeof User;
TypeScriptUserClass はざっくり言うと、こんな型です。
type UserClass = {
new (name: string, age: number): User; // コンストラクタ
fromName(name: string): User; // static メソッド
};
TypeScriptつまり、
typeof User は「コンストラクタ+static メンバーを持つ“クラス側の型”」
です。
ここから「インスタンスの型だけ」を取り出したいときに使うのがInstanceType です。
InstanceType<typeof クラス> で「インスタンス型」を取り出す
type UserInstance = InstanceType<typeof User>;
TypeScriptこの UserInstance は、実質こうなります。
type UserInstance = User;
TypeScriptつまり、
「typeof User(クラス側の型)から、“new した結果の型”だけを抜き出したもの」
が InstanceType<typeof User> です。
これだけだと「直接 User って書けばよくない?」と思うかもしれませんが、User という名前を直接使えない場面で威力を発揮します。
実用例1:クラスを変数に入れてからインスタンス型を取りたい
クラスを「引数や変数として扱う」とき
例えば、クラスを変数に入れて扱うコードを考えます。
class User {
constructor(public name: string) {}
}
const C = User;
const u = new C("Taro");
TypeScriptここで「C からインスタンスの型を取りたい」とします。
C の型はこう書けます。
type UserClass = typeof User;
const C2: UserClass = User;
TypeScriptこのとき、C2 のインスタンス型を取りたいなら、
type UserInstance = InstanceType<UserClass>;
TypeScriptと書けます。
つまり、
type UserClass = typeof User;
type UserInstance = InstanceType<UserClass>;
const C: UserClass = User;
const u: UserInstance = new C("Hanako");
TypeScriptという形です。
ここでのポイントは、
「クラスを“型パラメータ”や“変数の型”として扱っているとき、
そこからインスタンス型を取り出すのに InstanceType が使える」
ということです。
実用例2:ジェネリクスで「どんなクラスでもインスタンス型を取りたい」
コンストラクタを受け取る汎用関数
「どんなクラスでも受け取って、そのインスタンスを返す関数」を
ジェネリクスで書いてみます。
function create<C extends new (...args: any[]) => any>(
Ctor: C,
...args: ConstructorParameters<C>
): InstanceType<C> {
return new Ctor(...args);
}
TypeScript使い方はこうです。
class User {
constructor(public name: string) {}
}
class Product {
constructor(public id: number) {}
}
const u = create(User, "Taro"); // 型は User
const p = create(Product, 123); // 型は Product
TypeScriptここでの流れを整理すると、
C は「コンストラクタ型」(クラス側の型)InstanceType<C> は「そのコンストラクタが返すインスタンスの型」
という関係になっています。
InstanceType の定義は、ざっくり言うとこうです。
type InstanceType<T extends new (...args: any) => any> =
T extends new (...args: any) => infer R ? R : any;
TypeScriptつまり、
「new したときに返ってくる型(R)を推論して、それを取り出す」
というユーティリティ型です。
実用例3:クラスの配列から「要素のインスタンス型」を取りたい
クラスをまとめて扱うときの型
例えば、複数のクラスを配列で持っているとします。
class Dog {
speak() {
console.log("ワン");
}
}
class Cat {
speak() {
console.log("ニャー");
}
}
const animalClasses = [Dog, Cat] as const;
TypeScriptここから「インスタンスの型」を取りたいときに、InstanceType が使えます。
type AnimalClassUnion = (typeof animalClasses)[number];
// Dog | Cat の“クラス側の型”
type AnimalInstanceUnion = InstanceType<AnimalClassUnion>;
// Dog | Cat の“インスタンス側の型”
TypeScriptAnimalInstanceUnion は、
type AnimalInstanceUnion = Dog | Cat;
TypeScriptと同じ意味になります。
これで、
function makeAndSpeak(Ctor: AnimalClassUnion) {
const animal: AnimalInstanceUnion = new Ctor();
animal.speak();
}
TypeScriptのように、
「クラスの集合から、インスタンスの型の集合を導き出す」
ことができます。
「インスタンス型の取得」で混乱しないための整理
3つのレイヤーを意識する
頭の中で、次の3つをはっきり分けておくと混乱しにくくなります。
- インスタンスの型
User
「new User(...)で作られるオブジェクトの構造」 - クラス(コンストラクタ)の型
typeof Userやnew (...args) => User
「newできる“クラス側”の型」 - クラス型からインスタンス型を取り出すユーティリティ
InstanceType<typeof User>やInstanceType<C>
「コンストラクタ型から、“new した結果の型”だけを抜き出す」
特に大事なのは、
「InstanceType の引数は“クラス側の型”である」
という点です。
だからこそ、InstanceType<typeof User> という組み合わせがよく出てきます。
まとめ:インスタンス型の取得を自分の言葉で説明すると
最後に、あなた自身の言葉でこう整理してみてください。
クラスには「インスタンスの型」と「クラス(コンストラクタ)の型」がある。User はインスタンスの型、typeof User はクラスの型。InstanceType<T> は、「コンストラクタ型 T から、new した結果の型だけを取り出す」ユーティリティ型。
だから、
InstanceType<typeof User>は「User のインスタンス型」- ジェネリクスで
C extends new (...args) => anyと書いたとき、InstanceType<C>で「そのクラスのインスタンス型」が取れる
まずは、
type T = InstanceType<typeof User>;function f<C extends new (...args: any[]) => any>(c: C): InstanceType<C> { ... }
の2パターンを、自分の手で書いてみてください。
そこで、
「あ、InstanceType は“クラス側の型”から“インスタンス側”を抜き出すんだな」
と体で理解できたら、このテーマはほぼマスターできています。
