ゴール:「static は“インスタンスじゃなくてクラスに属するもの”だと腹で理解する」
static を一言でいうと、
「new した“個体”ではなく、“クラスそのもの”にぶら下がるもの」
です。
ここがふわっとしたままだと、
- いつ
User.nameで、いつuser.nameなのか - 何を static にして、何を普通のプロパティにするのか
がごちゃごちゃになります。
ここでは、
「インスタンス」と「クラスそのもの」をちゃんと分けながら、
static プロパティをかみ砕いていきます。
staticプロパティとは何か:インスタンスではなく“型そのもの”の持ち物
インスタンスのプロパティとの対比で理解する
まず、普通のプロパティから。
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
const u1 = new User("Taro");
const u2 = new User("Hanako");
console.log(u1.name); // "Taro"
console.log(u2.name); // "Hanako"
TypeScriptここでのポイントは、name は「インスタンスごとに違う値」を持てることです。
一方、static プロパティはこうなります。
class User {
static species = "Homo sapiens";
}
console.log(User.species); // "Homo sapiens"
const u = new User();
// u.species; // エラー:インスタンスからはアクセスできない
TypeScriptspecies は User クラスそのものにぶら下がっていて、new User() したインスタンスには存在しません。
つまり、
user.nameのようなものは「インスタンスの状態」User.speciesのようなものは「クラス全体に共通する情報」
という役割分担になります。
「インスタンスからは見えない」という感覚を持つ
もう少し強調すると、
static プロパティは「インスタンスの this には乗っていない」ものです。
class Counter {
static description = "カウンタークラス";
count = 0;
}
const c = new Counter();
console.log(c.count); // OK(インスタンスプロパティ)
console.log(Counter.description); // OK(staticプロパティ)
// console.log(c.description); // エラー
TypeScriptここで大事なのは、
「c という“実物”が持っているのは count だけ」
「Counter という“設計図そのもの”が持っているのが description」
というイメージです。
static を使うときは、
「これは“個体の情報”か?それとも“種としての情報”か?」
と自分に聞いてみると判断しやすくなります。
staticプロパティの型指定と初期化
型の書き方は普通のプロパティと同じ
static プロパティの型指定は、基本的に普通のプロパティと同じです。
class Config {
static appName: string = "MyApp";
static version: number = 1;
}
console.log(Config.appName); // "MyApp"
console.log(Config.version); // 1
TypeScript型だけ先に書いて、あとで代入することもできます。
class Config {
static appName: string;
}
Config.appName = "MyApp";
TypeScriptTypeScript 的には、
static appName: stringで「クラスに属する string 型のプロパティ」Config.appName = ...で初期化
という流れです。
readonly と組み合わせて「定数」として扱う
「アプリ名」「定数的な設定値」など、
変わってほしくない static プロパティは readonly と相性がいいです。
class Config {
static readonly appName = "MyApp";
static readonly defaultPageSize = 20;
}
// Config.appName = "Other"; // エラー:readonly
console.log(Config.appName);
TypeScriptstatic readonly にすると、
- クラスに属する
- 再代入できない
という「グローバル定数っぽい」性質を持たせられます。
「この値はアプリ全体で1つだけ」「変わると困る」
というものは、static readonly にすると設計がはっきりします。
staticプロパティの典型的な使いどころ
「全インスタンスで共有する情報」
例えば、「何人 User が作られたか」を数えたいとします。
class User {
static createdCount = 0;
constructor(
public name: string
) {
User.createdCount += 1;
}
}
const u1 = new User("Taro");
const u2 = new User("Hanako");
console.log(User.createdCount); // 2
TypeScriptここでのポイントは、
createdCountは「User 全体で1つだけ」- 各インスタンスが作られるたびに、そのカウンタを増やしている
ということです。
もしこれをインスタンスプロパティにしてしまうと、
「インスタンスごとにカウンタがバラバラ」になってしまいます。
「全インスタンスで共有したい情報」は、
static プロパティにするのが自然です。
「クラスに紐づく設定・メタ情報」
例えば、「このクラスが扱うリソース名」や「テーブル名」など。
class UserModel {
static tableName = "users";
constructor(
public id: number,
public name: string
) {}
}
console.log(UserModel.tableName); // "users"
TypeScriptここで tableName をインスタンスに持たせる必要はありません。
どのインスタンスでも同じ値だからです。
「このクラスが表しているものに関する“ラベル”や“設定”」は、
static プロパティにするとスッキリします。
staticプロパティとstaticメソッドのセット使い
「クラスに関する操作」は static に寄せる
static プロパティと一緒によく使うのが static メソッドです。
class User {
static createdCount = 0;
constructor(
public name: string
) {
User.createdCount += 1;
}
static getCreatedCount(): number {
return User.createdCount;
}
}
new User("Taro");
new User("Hanako");
console.log(User.getCreatedCount()); // 2
TypeScriptここでは、
createdCount:クラスに属する状態getCreatedCount:その状態を扱うクラスメソッド
というセットになっています。
「インスタンスではなく“クラスそのもの”に関する操作」は、
static プロパティ+static メソッドでまとめると、
責務の分担がきれいになります。
インスタンスメソッドからstaticプロパティを使う
もちろん、インスタンスメソッドから static プロパティを参照することもできます。
class User {
static species = "Homo sapiens";
constructor(
public name: string
) {}
introduce(): void {
console.log(`私は${User.species}の${this.name}です`);
}
}
const u = new User("Taro");
u.introduce(); // 私はHomo sapiensのTaroです
TypeScriptここでのポイントは、
- インスタンスメソッドの中でも、static には
User.〜でアクセスする this.speciesではない(this はインスタンスだから)
というところです。
「インスタンスの情報」と「クラス全体の情報」を混ぜないように、
アクセスの書き方も意識しておくと混乱しにくくなります。
staticプロパティを乱用しないための感覚
「グローバル変数の代わり」にしない
static プロパティは便利ですが、
なんでもかんでも突っ込むと「クラスにくっついたグローバル変数」になります。
例えば、こんなコードは危険信号です。
class AppState {
static currentUser: User | null = null;
static isDebugMode = false;
static cache: any = {};
}
TypeScriptどこからでも AppState.currentUser を書き換えられるようにしてしまうと、
- 依存関係が見えづらい
- どこで変わったのか追いにくい
- テストもしづらい
という状態になります。
「どこからでも触れる共有状態」を static に持たせるのは、
かなり慎重に考えたほうがいいです。
staticにするか迷ったときの問い
迷ったら、こう自問してみてください。
「これは“個体ごとの状態”か?
それとも“クラス全体で1つだけあればいい情報”か?」
前者ならインスタンスプロパティ、
後者なら static プロパティ、
という分け方が基本です。
さらに、
「この値は、外から自由に書き換えられていいか?」
と考えて、
ダメなら readonly や private を組み合わせる、
という流れで設計していくと、
static が“いい意味で地味な存在”になってくれます。
まとめ:staticプロパティを自分の言葉で言うと
最後に、あなた自身の言葉でこう整理してみてください。
static プロパティは、
newしたインスタンスではなく、「クラスそのもの」に属する値- アクセスは
User.xxxのように「クラス名.プロパティ名」で行う - 「全インスタンスで共有する情報」や「クラスに紐づく設定・メタ情報」に向いている
static readonlyにすると「クラスレベルの定数」として扱える- 乱用すると「クラス付きのグローバル変数」になってしまうので、
「本当にクラス全体で1つだけでいい情報か?」を毎回確認する
今書いているクラスを1つ選んで、
「これは全インスタンスで共通だよな」という情報がないか探してみてください。
もしあれば、それを static プロパティにしてみる。
逆に、「なんとなく static にしていたけど、実はインスタンスごとに違うべきだな」というものがあれば、
それを普通のプロパティに戻してみる。
その小さな調整が、
クラスを「ただ動くコード」から、
“インスタンスとクラスの役割がきちんと分かれた設計” に変えていきます。
