TypeScript | 関数・クラス・ジェネリクス:クラス設計 – staticプロパティ

TypeScript TypeScript
スポンサーリンク

ゴール:「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; // エラー:インスタンスからはアクセスできない
TypeScript

speciesUser クラスそのものにぶら下がっていて、
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";
TypeScript

TypeScript 的には、

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

static 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 プロパティ、
という分け方が基本です。

さらに、

「この値は、外から自由に書き換えられていいか?」

と考えて、
ダメなら readonlyprivate を組み合わせる、
という流れで設計していくと、
static が“いい意味で地味な存在”になってくれます。


まとめ:staticプロパティを自分の言葉で言うと

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

static プロパティは、

  • new したインスタンスではなく、「クラスそのもの」に属する値
  • アクセスは User.xxx のように「クラス名.プロパティ名」で行う
  • 「全インスタンスで共有する情報」や「クラスに紐づく設定・メタ情報」に向いている
  • static readonly にすると「クラスレベルの定数」として扱える
  • 乱用すると「クラス付きのグローバル変数」になってしまうので、
    「本当にクラス全体で1つだけでいい情報か?」を毎回確認する

今書いているクラスを1つ選んで、
「これは全インスタンスで共通だよな」という情報がないか探してみてください。

もしあれば、それを static プロパティにしてみる。
逆に、「なんとなく static にしていたけど、実はインスタンスごとに違うべきだな」というものがあれば、
それを普通のプロパティに戻してみる。

その小さな調整が、
クラスを「ただ動くコード」から、
“インスタンスとクラスの役割がきちんと分かれた設計” に変えていきます。

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