TypeScript | 関数・クラス・ジェネリクス:クラス設計 – クラスの型としての扱い

TypeScript TypeScript
スポンサーリンク

ゴール:「クラスには“2つの顔(値としての顔/型としての顔)”がある」と腑に落とす

TypeScript のクラスで一番つまずきやすいポイントは、

「クラスは“値”でもあり、“型”でもある」

という二重の顔を持っていることです。

ここをちゃんと分けて理解できると、

  • 関数の引数や戻り値に「クラスの型」を使う
  • 「インスタンスの型」と「クラスそのものの型」を区別する
  • コンストラクタ型(new (...) => ...)を自然に読める

ようになっていきます。


クラスの「インスタンスの型」としての扱い

一番素直な使い方:インスタンスの型として使う

まずは、いちばん直感的な「クラスの型としての使い方」から。

class User {
  constructor(public name: string, public age: number) {}

  greet(): void {
    console.log(`こんにちは、${this.name}です`);
  }
}

const u = new User("Taro", 20);

// ここでの User は「インスタンスの型」として使える
function printUser(user: User): void {
  console.log(user.name, user.age);
  user.greet();
}

printUser(u);
TypeScript

このときの User は、

name: stringage: number を持ち、greet() を呼べるオブジェクトの型」

として扱われています。

つまり、

  • new User(...) で作られる“インスタンスの形”
  • それを表す「型」としての User

が一致している、ということです。

ここまでは、たぶん違和感ないと思います。

クラスの「インスタンス型」は“中身の構造”を表している

もう少しだけ踏み込むと、

let user: User;
TypeScript

と書いたときの User は、

User クラスで作られたもの」ではなく、
User クラスのインスタンスと同じ構造を持つもの」
という意味になります。

極端な話、こんなこともできます。

const obj: User = {
  name: "Hanako",
  age: 30,
  greet() {
    console.log("こんにちは、オブジェクト版です");
  },
};
TypeScript

new User で作っていなくても、
「構造が同じなら User 型として扱える」のが TypeScript の特徴です(構造的型付け)。


クラスの「値としての型」(コンストラクタ型)としての扱い

クラスそのものを変数に入れるときの型

次に、「クラスそのもの」を扱う場合を見てみます。

class User {
  constructor(public name: string) {}
}

const C = User; // クラスを“値”として代入

const u = new C("Taro");
TypeScript

ここでの C は、

「`new (name: string) できて、User インスタンスを返す値」

です。

型で書くとこうなります。

type UserConstructor = {
  new (name: string): User;
};

const C2: UserConstructor = User;
const u2 = new C2("Hanako");
TypeScript

このときの UserConstructor は、

「コンストラクタとして呼べる“クラスの型”」

を表しています。

ここが、

  • User(インスタンスの型)
  • UserConstructor(クラスそのものの型=コンストラクタ型)

の違いです。

「インスタンスの型」と「クラスの型」を並べて見る

整理すると、こういう関係になっています。

class User {
  constructor(public name: string) {}
}

// インスタンスの型
let u: User;

// クラスそのものの型(コンストラクタ型)
type UserConstructor = new (name: string) => User;
let C: UserConstructor = User;
TypeScript

同じ User という名前でも、

  • User 単体 → 「インスタンスの型」
  • new (...) => User → 「クラス(コンストラクタ)の型」

というふうに、役割が違うことを意識しておくと混乱しにくくなります。


関数の引数・戻り値として「クラスの型」を扱う

「インスタンスを受け取る関数」と「クラスを受け取る関数」

例で比べてみます。

インスタンスを受け取る関数:

class User {
  constructor(public name: string) {}
}

function greetUser(user: User): void {
  console.log(`こんにちは、${user.name}さん`);
}

const u = new User("Taro");
greetUser(u);
TypeScript

ここでの User は「インスタンスの型」です。

一方、「クラスそのもの」を受け取る関数はこうなります。

type UserConstructor = new (name: string) => User;

function createAndGreet(Ctor: UserConstructor, name: string): User {
  const user = new Ctor(name);
  console.log(`こんにちは、${user.name}さん`);
  return user;
}

createAndGreet(User, "Hanako");
TypeScript

ここでの Ctor は、

new (name: string) できるクラス(コンストラクタ)」

という型になっています。

同じ「User に関係する型」でも、

  • User → インスタンスの型
  • UserConstructor → クラスの型(コンストラクタ)

というふうに、使い分けているわけです。

ジェネリクスと組み合わせると「どんなクラスでも受け取れる」

もう少しだけ進んで、
「どんなクラスでも受け取れる関数」を考えてみます。

type Constructor<T> = new (...args: any[]) => T;

function createInstance<T>(Ctor: Constructor<T>, ...args: any[]): T {
  return new Ctor(...args);
}

class User {
  constructor(public name: string) {}
}

class Product {
  constructor(public id: number) {}
}

const u = createInstance(User, "Taro");
const p = createInstance(Product, 123);
TypeScript

ここでのポイントは、

  • Constructor<T> が「T を返すコンストラクタの型」
  • createInstance は「どんなクラスでも受け取って、インスタンスを返す」汎用関数

になっていることです。

このときも、

  • UserProduct は「クラスそのもの(値)」として渡されている
  • その型は「コンストラクタ型」として表現されている

という構造になっています。


クラスの「静的メンバーの型」としての扱い

クラスの「インスタンス側」と「静的側」は型が別物

クラスには、

  • インスタンス側(this でアクセスするプロパティ・メソッド)
  • 静的側(ClassName.xxx でアクセスする static メンバー)

の2つがあります。

class Counter {
  static total = 0;

  constructor(public value: number) {
    Counter.total += value;
  }
}
TypeScript

このとき、

  • Counter → 「クラスそのもの(値)」
  • Counter.total → 「静的プロパティ」
  • new Counter(1) → 「インスタンス」
  • インスタンスの型 → Counter

という関係になります。

静的側の型を取りたいときは、typeof を使います。

type CounterStatic = typeof Counter;

function printTotal(C: CounterStatic): void {
  console.log(C.total);
}

printTotal(Counter);
TypeScript

ここでの CounterStatic は、

total という static プロパティを持つ“クラスの型”」

です。

typeof クラス名 は、

「そのクラス“自体”の型(静的メンバーを含む)」

を表す、というのを覚えておくと便利です。


まとめ:「クラスの型としての扱い」を自分の言葉で整理すると

最後に、あなた自身の言葉でこう整理してみてください。

クラスには 2 つの顔がある。

1つは「インスタンスの型」としての顔。
user: User のように書くときの User は、
new User(...) で作られるオブジェクトと同じ構造を持つ型」を意味する。

もう 1 つは「クラスそのもの(コンストラクタ)の型」としての顔。
new (name: string) => User のような形で表現され、
new できる値」としてのクラスを扱うときに使う。
typeof クラス名 を使うと、「静的メンバーを含んだクラス自体の型」も取れる。

まずは、

  • インスタンスを受け取る関数((u: User) => void
  • クラスを受け取る関数((Ctor: new (...) => User) => User

を自分で 1 つずつ書き分けてみると、
「クラスの型としての扱い」の感覚がかなりクリアになってきます。

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