TypeScript | 関数・クラス・ジェネリクス:クラス設計 – InstanceType の利用

TypeScript TypeScript
スポンサーリンク

ゴール:「クラス“側”だけ知っていても、インスタンスの型を安全に扱える」ようになる

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);
TypeScript

UserClass は「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 Classnew (...args) => any)しか手元にない」状況から、
インスタンスの型を安全に導きたいときに使う。

まずは次の 2 つを、自分の手で書いてみてください。

type T1 = InstanceType<typeof User>;
function f<C extends new (...args: any[]) => any>(c: C): InstanceType<C> { ... }

そこで、

InstanceType は“クラス側”から“インスタンス側”を引き出す橋なんだな」

と感じられたら、もうこの型は十分“使える”レベルにいます。

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