ゴール:「クラス“側”だけ知っていても、インスタンスの型を安全に扱える」ようになる
InstanceType<T> は一言でいうと、
「コンストラクタ型 T から、new したときの“インスタンスの型”だけを取り出すユーティリティ型」
です。
これが効いてくるのは、
- 「クラスそのもの(
typeof Class)しか手元にない」 - 「でも、そのインスタンスの型で変数や引数を型付けしたい」
という場面です。
ここでは、初心者でもイメージしやすい具体例を通して、InstanceType を「どこで・どう使うと気持ちいいか」にフォーカスして解説します。
基本のおさらい:typeof Class と InstanceType の関係
クラス側の型とインスタンス側の型
まずはシンプルなクラスから。
class User {
constructor(public name: string, public age: number) {}
greet(): void {
console.log(`こんにちは、${this.name}です`);
}
}
TypeScriptここには 2 つの世界があります。
インスタンス側の型としての User。
const u: User = new User("Taro", 20);
u.greet();
TypeScriptこれは「new User(...) で作られるオブジェクトの型」です。
クラス側(コンストラクタ側)の型としての typeof User。
const C = User; // 値としてのクラス
type UserClass = typeof User;
const c: UserClass = User;
const u2 = new c("Hanako", 30);
TypeScriptUserClass は「new (name: string, age: number) できるクラス」の型です。
ここから「インスタンスの型だけ」を取り出したいときに使うのが InstanceType です。
InstanceType<typeof User> は「User のインスタンス型」
type UserInstance = InstanceType<typeof User>;
TypeScriptこの UserInstance は、実質 User と同じ型になります。
重要なのは、
InstanceType の引数は「クラス側の型(コンストラクタ型)」である
という点です。
だから、InstanceType<typeof User> という組み合わせがよく出てきます。
利用パターン1:クラスを変数で扱うときに「インスタンス型」を失わない
クラスを変数に入れた瞬間、型がややこしくなる
例えば、こんなコードを書いたとします。
class User {
constructor(public name: string) {}
}
const C = User;
const u = new C("Taro");
TypeScriptここで「u の型をちゃんと書きたい」と思ったとき、User という名前を直接使えない状況も出てきます(ジェネリクスや外部から渡される場合など)。
そんなときに InstanceType が効きます。
type UserClass = typeof User;
type UserInstance = InstanceType<UserClass>;
const C2: UserClass = User;
const u2: UserInstance = new C2("Hanako");
TypeScriptここでやっていることは、
UserClass= クラス側の型(コンストラクタ)InstanceType<UserClass>= そのコンストラクタが返すインスタンスの型
という変換です。
ポイントは、
「クラスを変数として扱っても、InstanceType を通せばインスタンス型をきれいに取り戻せる」
というところです。
利用パターン2:ジェネリクスで「どんなクラスでもインスタンスを返す関数」
コンストラクタを受け取って、そのインスタンスを返す
InstanceType が一番“らしく”使えるのは、ジェネリクスと組み合わせたときです。
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 でインスタンス型を自動で導く」
という使い方です。
これを手書きでやろうとすると、
function createUser(Ctor: typeof User, name: string): User { ... }
function createProduct(Ctor: typeof Product, id: number): Product { ... }
TypeScriptのように、クラスごとに関数を増やす必要があります。
InstanceType を使えば、
「どんなクラスでも 1 本の関数で済む」ようになります。
利用パターン3:クラスの配列から「インスタンスの union 型」を作る
クラスの集合 → インスタンスの集合
複数のクラスをまとめて扱うときにも InstanceType は便利です。
class Dog {
speak() {
console.log("ワン");
}
}
class Cat {
speak() {
console.log("ニャー");
}
}
const animalClasses = [Dog, Cat] as const;
TypeScriptここから「インスタンスの型」を作りたいとします。
type AnimalClassUnion = (typeof animalClasses)[number];
// Dog | Cat の“クラス側の型”
type AnimalInstanceUnion = InstanceType<AnimalClassUnion>;
// Dog | Cat の“インスタンス側の型”
TypeScriptこれで、
function makeAndSpeak(Ctor: AnimalClassUnion) {
const animal: AnimalInstanceUnion = new Ctor();
animal.speak();
}
TypeScriptのように、
「どのクラスが来ても、そのインスタンスは speak() を持つ」
ということを型で表現できます。
ここでもやっていることは同じで、
- まず「クラス側の union 型」を作る
- そこから
InstanceTypeで「インスタンス側の union 型」を導く
という流れです。
利用パターン4:DI コンテナ風に「登録されたクラスからインスタンスを返す」
名前 → クラス → インスタンス、を型安全に
簡易的な「クラス登録&生成」の仕組みを考えてみます。
class UserService {
getName() {
return "UserService";
}
}
class ProductService {
getName() {
return "ProductService";
}
}
const registry = {
user: UserService,
product: ProductService,
} as const;
TypeScriptここから、「キーに応じたインスタンス」を返す関数を作ります。
type ServiceRegistry = typeof registry;
type ServiceKey = keyof ServiceRegistry;
type ServiceClass<K extends ServiceKey> = ServiceRegistry[K];
type ServiceInstance<K extends ServiceKey> = InstanceType<ServiceClass<K>>;
function resolve<K extends ServiceKey>(key: K): ServiceInstance<K> {
const Ctor = registry[key];
return new Ctor();
}
const s1 = resolve("user"); // 型は UserService
const s2 = resolve("product"); // 型は ProductService
TypeScriptここで InstanceType は、
ServiceClass<K>(クラス側の型)からServiceInstance<K>(インスタンス側の型)を導く
役割を担っています。
これにより、
「キーに応じて正しいクラスが選ばれ、そのインスタンス型も自動で合う」
という、かなりリッチな型付けが実現できます。
どんなときに InstanceType を使うべきか
キーワードは「クラス側しか持っていないのに、インスタンス型が欲しいとき」
InstanceType を使うかどうか迷ったら、
自分の状況をこう言い換えてみてください。
「今、自分が持っているのは“クラスそのもの(コンストラクタ)”の型だ。
でも、欲しいのは“そのインスタンスの型”だ。」
例えば、こんなときです。
クラスを引数として受け取っている。
クラスを配列やオブジェクトに詰めている。
ジェネリクスで「コンストラクタ型」を扱っている。
こういうときに、
「コンストラクタ型 → インスタンス型」
の変換をしてくれるのが InstanceType です。
まとめ:InstanceType の利用を自分の言葉で整理すると
最後に、あなた自身の言葉でこうまとめてみてください。
InstanceType<T> は、「コンストラクタ型 T から、new した結果の型だけを取り出す」ユーティリティ型。
だから、InstanceType<typeof User> は「User のインスタンス型」になる。
クラスを変数やジェネリクスで扱うとき、
「クラス側の型(typeof Class や new (...args) => any)しか手元にない」状況から、
インスタンスの型を安全に導きたいときに使う。
まずは次の 2 つを、自分の手で書いてみてください。
type T1 = InstanceType<typeof User>;function f<C extends new (...args: any[]) => any>(c: C): InstanceType<C> { ... }
そこで、
「InstanceType は“クラス側”から“インスタンス側”を引き出す橋なんだな」
と感じられたら、もうこの型は十分“使える”レベルにいます。
