ゴール:「クラスには“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: string と age: number を持ち、greet() を呼べるオブジェクトの型」
として扱われています。
つまり、
new User(...)で作られる“インスタンスの形”- それを表す「型」としての
User
が一致している、ということです。
ここまでは、たぶん違和感ないと思います。
クラスの「インスタンス型」は“中身の構造”を表している
もう少しだけ踏み込むと、
let user: User;
TypeScriptと書いたときの User は、
「User クラスで作られたもの」ではなく、
「User クラスのインスタンスと同じ構造を持つもの」
という意味になります。
極端な話、こんなこともできます。
const obj: User = {
name: "Hanako",
age: 30,
greet() {
console.log("こんにちは、オブジェクト版です");
},
};
TypeScriptnew 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は「どんなクラスでも受け取って、インスタンスを返す」汎用関数
になっていることです。
このときも、
UserやProductは「クラスそのもの(値)」として渡されている- その型は「コンストラクタ型」として表現されている
という構造になっています。
クラスの「静的メンバーの型」としての扱い
クラスの「インスタンス側」と「静的側」は型が別物
クラスには、
- インスタンス側(
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 つずつ書き分けてみると、
「クラスの型としての扱い」の感覚がかなりクリアになってきます。
