TypeScript | 関数・クラス・ジェネリクス:クラス設計 – インスタンス型の取得

TypeScript TypeScript
スポンサーリンク

ゴール:「“クラスそのもの”から“インスタンスの型”だけをきれいに取り出す」感覚をつかむ

ここでのテーマは、

「クラス(コンストラクタ)から、そのインスタンスの型だけを取り出す」

です。

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

UserClass はざっくり言うと、こんな型です。

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 の“インスタンス側の型”
TypeScript

AnimalInstanceUnion は、

type AnimalInstanceUnion = Dog | Cat;
TypeScript

と同じ意味になります。

これで、

function makeAndSpeak(Ctor: AnimalClassUnion) {
  const animal: AnimalInstanceUnion = new Ctor();
  animal.speak();
}
TypeScript

のように、

「クラスの集合から、インスタンスの型の集合を導き出す」

ことができます。


「インスタンス型の取得」で混乱しないための整理

3つのレイヤーを意識する

頭の中で、次の3つをはっきり分けておくと混乱しにくくなります。

  1. インスタンスの型
    User
    new User(...) で作られるオブジェクトの構造」
  2. クラス(コンストラクタ)の型
    typeof Usernew (...args) => User
    new できる“クラス側”の型」
  3. クラス型からインスタンス型を取り出すユーティリティ
    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 は“クラス側の型”から“インスタンス側”を抜き出すんだな」

と体で理解できたら、このテーマはほぼマスターできています。

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