1. ホーム
  2. TypeScript

【TypeScript】static メンバーの使い方|インスタンス化せずに使えるプロパティ・メソッドを定義する

Share

クラスを使っていると、「これはインスタンスごとに持たせる必要はないな」と感じるプロパティやメソッドが出てきます。たとえば円周率のような定数や、状態に依存しない計算だけの関数です。こうした「クラス自体に持たせたいもの」を定義するのが static メンバー(静的メンバー)です。この記事では、static プロパティ・static メソッドの基本的な書き方から、static readonly による定数、static 初期化ブロック、private static による内部状態の共有、インスタンスを生成して返すファクトリメソッド、そして継承時の振る舞いまでを、動くコードとあわせて解説します。

static メンバーとは何か

通常、クラスに定義したプロパティやメソッドは、new で作ったインスタンス(個々のオブジェクト)に属します。これに対して static を付けたメンバーは、インスタンスではなくクラスそのものに属します。そのため、インスタンスを作らなくても、クラス名から直接呼び出せます。

たとえば数値計算のユーティリティをまとめた MathUtil というクラスがあるとき、MathUtil.PIMathUtil.square(3) のように、クラス名にドットをつないで使います。new MathUtil() でインスタンスを作る必要はありません。「そのクラスに関係はあるが、個々のオブジェクトの状態とは無関係」なものを置く場所、と考えると分かりやすいでしょう。

なぜ static メンバーを使うのか

static メンバーが役立つのは、インスタンスごとの状態に依存しない機能を、クラスにまとめて置きたいときです。代表的な用途を下の表に整理します。

用途説明
ユーティリティ関数入力を受け取って結果を返すだけで、インスタンスの状態を使わない計算・変換処理
定数円周率や設定値など、クラスに関連する固定値。static readonly で書き換え不可にできる
共有カウンタ・共有状態「これまで何個インスタンスを作ったか」など、すべてのインスタンスで1つだけ共有したい値
ファクトリメソッドnew の代わりに、インスタンスを組み立てて返す入り口となるメソッド

いずれも「インスタンスを1つ作らないと使えない」のは不自然なものばかりです。static にしておくことで、new の手間なくクラス名から直接呼べて、意図もはっきりします。

static プロパティと static メソッドの基本

まずは最小の例です。プロパティやメソッドの前に static を付けるだけで、それがクラス側のメンバーになります。呼び出すときはインスタンスではなくクラス名を使います。

math-util.ts
class MathUtil {
  // static プロパティ(クラスに属する値)
  static PI = 3.14159;

  // static メソッド(クラスに属する関数)
  static square(n: number): number {
    return n * n;
  }
}

// インスタンスを作らず、クラス名から直接呼び出す
console.log(MathUtil.PI);        // 3.14159
console.log(MathUtil.square(3)); // 9

ここで押さえておきたいのは、static メンバーはインスタンスからは呼び出せないという点です。new MathUtil() で作ったインスタンス経由で square を呼ぼうとすると、TypeScript はコンパイルエラーにします。

math-util-error.ts
const util = new MathUtil();

// エラー: プロパティ 'square' は型 'MathUtil' に存在しません。
util.square(3);

// 正しくはクラス名から呼ぶ
MathUtil.square(3); // OK

逆に、インスタンスプロパティ(static なしのメンバー)はクラス名からは呼べません。static かどうかで「どこから呼ぶか」がはっきり分かれる、と覚えておくと混乱しません。

static メソッド内の this はクラス自身を指す

static メソッドの中で this を使うと、それはインスタンスではなくクラスそのものを指します。そのため、同じクラスの別の static メンバーを this 経由で参照できます。

circle-util.ts
class CircleUtil {
  static PI = 3.14159;

  static area(radius: number): number {
    // static メソッド内の this は CircleUtil クラス自身
    return this.PI * radius * radius;
  }
}

console.log(CircleUtil.area(2)); // 12.56636

上の this.PICircleUtil.PI と同じ意味になります。クラス名を直接書いても動きますが、this を使うと、後述する継承でサブクラスが値を上書きした場合にもうまく追従します。static メソッドの this はインスタンスではない、という点だけ最初にしっかり区別しておきましょう。

static readonly で定数を定義する

定数のように「一度決めたら書き換えたくない値」は、staticreadonly を組み合わせて定義します。readonly を付けると、クラス外からはもちろん、クラス内からも初期化後の再代入ができなくなり、うっかりした変更を防げます。

app-config.ts
class AppConfig {
  static readonly VERSION = '1.0.0';
  static readonly MAX_RETRY = 3;
}

console.log(AppConfig.VERSION); // 1.0.0

// エラー: 読み取り専用プロパティであるため、'VERSION' に代入することはできません。
AppConfig.VERSION = '2.0.0';

設定値やバージョン番号、リトライ回数の上限といった「クラスに紐づく固定値」は、この static readonly でまとめておくと、参照箇所が AppConfig.MAX_RETRY のように読みやすくなり、値の変更もこのクラス1か所を見れば済みます。

static 初期化ブロックで複雑な初期化をする

static プロパティの初期値が、1行の式では書ききれないこともあります。複数の計算を挟んだり、条件によって値を組み立てたりしたい場合に使えるのが、static 初期化ブロックstatic { ... })です。これは TypeScript 4.4 以降で使える機能で、クラスが定義されるタイミングで一度だけ実行されます。

settings.ts
class Settings {
  static readonly items: string[];
  static readonly count: number;

  // static 初期化ブロック(クラス定義時に一度だけ実行される)
  static {
    const base = ['theme', 'language'];
    // 条件によって初期化内容を組み立てる
    if (process.env.NODE_ENV === 'development') {
      base.push('debug');
    }
    this.items = base;
    this.count = base.length;
  }
}

console.log(Settings.items); // ['theme', 'language', ...]
console.log(Settings.count); // 2 または 3

ブロック内の this も、static メソッドと同じくクラス自身を指します。そのため this.items への代入で static プロパティを初期化できます。単純な代入で足りるなら初期化ブロックは不要ですが、途中に処理を挟みたい初期化では、このブロックを使うと読みやすくまとまります。

private static でクラス内部だけの状態を共有する

static プロパティはすべてのインスタンスで共有されるため、「これまで何個インスタンスを作ったか」といったカウンタに向いています。ただし、その値を外部から勝手に書き換えられると困る場合は、外に公開しないようにします。クラスの外から触れないようにするには、TypeScript の private static、または JavaScript 標準の static #name(プライベートフィールド)を使います。

user-counter.ts
class User {
  // クラス内部だけで共有するカウンタ(外部からは触れない)
  static #count = 0;

  constructor(public name: string) {
    // インスタンスが作られるたびに 1 増やす
    User.#count++;
  }

  // 現在の生成数を返す static メソッド
  static getCount(): number {
    return User.#count;
  }
}

new User('Alice');
new User('Bob');

console.log(User.getCount()); // 2

static #count はクラスの外からは User.#count と書いてもアクセスできず、値の読み取りは getCount() という window(入り口)を通してのみ行えます。こうすると、カウンタの増減ロジックをクラス内に閉じ込められます。# を使ったプライベートフィールドはコンパイル後の JavaScript でも本当にアクセスできませんが、private static は TypeScript のコンパイル時チェックによる制限である、という違いも押さえておくとよいでしょう。

ファクトリメソッドでインスタンスを生成する

static メソッドは、new の代わりにインスタンスを組み立てて返す「入り口」としてもよく使われます。これをファクトリメソッドと呼びます。コンストラクタをそのまま呼ぶよりも、生成方法に名前を付けられる(意図が伝わる)、生成前に検証や変換を挟める、といった利点があります。

product.ts
class Product {
  private constructor(
    public name: string,
    public price: number,
  ) {}

  // JSON 文字列からインスタンスを組み立てて返すファクトリメソッド
  static fromJson(json: string): Product {
    const data = JSON.parse(json);
    return new Product(data.name, data.price);
  }

  // 割引後の価格でインスタンスを作るファクトリメソッド
  static withDiscount(name: string, price: number, rate: number): Product {
    return new Product(name, Math.floor(price * (1 - rate)));
  }
}

const p1 = Product.fromJson('{"name":"Book","price":1200}');
const p2 = Product.withDiscount('Pen', 200, 0.1);

console.log(p1.price); // 1200
console.log(p2.price); // 180

この例では、コンストラクタを private にして直接の new Product(...) を禁じ、生成の窓口を fromJsonwithDiscount に絞っています。fromJson(JSON から作る)、withDiscount(割引価格で作る)のように名前で生成方法を表せるので、呼び出し側のコードが読みやすくなります。static メソッドの中では、そのクラスの private なコンストラクタにもアクセスできる点がポイントです。

static メンバーは継承される

つまずきやすいのが、static メンバーとサブクラスの関係です。static メンバーはサブクラスにも引き継がれ、サブクラスのクラス名からそのまま呼び出せます。

inheritance.ts
class Animal {
  static describe(): string {
    return 'これは動物です';
  }
}

class Dog extends Animal {}

// 親クラスの static メソッドを、サブクラスのクラス名から呼べる
console.log(Dog.describe()); // これは動物です

ここで注意したいのは、static なプロパティを親子で共有しているケースです。可変な static プロパティを親クラスに持たせると、サブクラスがそれを更新したときに親側にも影響が及ぶことがあります。次のカウンタの例を見てください。

shared-counter.ts
class Base {
  static count = 0;
  static increment() {
    // this は呼び出したクラスを指す
    this.count++;
  }
}

class Child extends Base {}

Base.increment();  // Base.count を増やす
Child.increment(); // Child は独自の count を持つ

console.log(Base.count);  // 1
console.log(Child.count); // 1

この例では、Child が最初に count を書き換えたタイミングで Child 側に独自の count が作られるため、Base.countChild.count は別々の値になります。this.count++this が「呼び出したクラス」を指すことで、書き込み先が呼び出し元ごとに分かれるためです。継承と可変な static プロパティが組み合わさると、どのクラスの値を触っているのかが分かりにくくなります。共有カウンタのような可変状態は、継承を前提にせず1つのクラスに閉じておくと、こうした混乱を避けられます。

まとめ

static メンバーは、インスタンスではなくクラス自体に属するプロパティやメソッドで、MathUtil.PIMathUtil.square(3) のようにクラス名から直接呼び出します。インスタンスの状態に依存しないユーティリティ関数、static readonly による定数、インスタンスをまたいで共有するカウンタなどに向いており、インスタンスからは呼び出せない点が特徴です。static メソッド内の this はクラス自身を指し、複雑な初期化には static { ... } ブロック、外に公開したくない状態には private staticstatic #name が使えます。new の代わりにインスタンスを組み立てて返すファクトリメソッドも static の定番です。継承すると static メンバーはサブクラスからも呼べますが、可変な static プロパティは親子で意図せず分かれたり共有されたりするため、共有状態の扱いには注意しましょう。

参考ページ