TypeScript | 関数・クラス・ジェネリクス:クラス設計 – staticメソッド

TypeScript TypeScript
スポンサーリンク

ゴール:「これは“インスタンスのメソッド”か“クラスのメソッド”か」を自分で選べるようになる

static メソッドは一言でいうと、

「new した“個体”ではなく、“クラスそのもの”にぶら下がる関数」

です。

ここがあいまいなままだと、

  • いつ user.doSomething()
  • いつ User.doSomething() なのか

がごちゃごちゃになります。

逆に、ここをちゃんと分けて考えられるようになると、
クラス設計の気持ちよさが一段変わります。


staticメソッドの基本:インスタンスではなく「クラス名から呼ぶ関数」

インスタンスメソッドとの対比で理解する

まず、普通の(インスタンス)メソッドから。

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

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

const u = new User("Taro");
u.greet(); // インスタンスから呼ぶ
TypeScript

greet は「インスタンスごとの状態(this.name)」を使うメソッドなので、
new User(...) した“個体”から呼びます。

static メソッドはこうなります。

class User {
  static createGuest(): User {
    return new User("ゲスト");
  }

  constructor(
    public name: string
  ) {}
}

const guest = User.createGuest(); // クラス名から呼ぶ
console.log(guest.name); // "ゲスト"
TypeScript

ここでのポイントは、

  • createGueststatic が付いている
  • 呼び出しは User.createGuest()(インスタンスではなくクラス名)
  • メソッドの戻り値の型は普通の関数と同じように書く(ここでは User

というところです。

「インスタンスに紐づく処理」なら普通のメソッド、
「クラスそのものに紐づく処理」なら static メソッド

という切り分けを意識すると、だいぶ見通しがよくなります。

staticメソッドの型指定は「普通の関数」と同じ

型の書き方自体は、関数とまったく同じです。

class MathUtil {
  static add(a: number, b: number): number {
    return a + b;
  }
}

const result = MathUtil.add(1, 2); // number
TypeScript
  • 引数に型を付ける
  • 戻り値に型を付ける

この2つを、インスタンスメソッドと同じように書くだけです。

違うのは「どこから呼ぶか」だけ。
new したインスタンスではなく、クラス名から呼びます。


staticメソッドが「向いている処理」とは何か

「インスタンスの状態に依存しない処理」

一番わかりやすい基準はこれです。

「this(インスタンスの状態)を使わない処理なら、static にできる」

例えば、文字列をフォーマットするだけの処理。

class StringUtil {
  static toSnakeCase(value: string): string {
    return value.replaceAll(" ", "_").toLowerCase();
  }
}

const s = StringUtil.toSnakeCase("Hello World"); // "hello_world"
TypeScript

ここでは、this を一切使っていません。
「ただのユーティリティ関数」です。

こういう「インスタンスに紐づかない処理」は、
クラスにぶら下げるなら static メソッドにするのが自然です。

「インスタンスを“作る”ための工場メソッド」

もう一つ、static メソッドがよく使われるのが「生成系」です。

class User {
  constructor(
    public name: string,
    public isAdmin: boolean
  ) {}

  static createAdmin(name: string): User {
    return new User(name, true);
  }

  static createNormal(name: string): User {
    return new User(name, false);
  }
}

const admin = User.createAdmin("Taro");
const normal = User.createNormal("Hanako");
TypeScript

ここでの設計はこうです。

  • コンストラクタは「最低限の情報」を受け取る
  • static メソッドは「よくある作り方」を名前付きで提供する

createAdmin / createNormal のように名前を付けることで、
「どういう前提の User を作るのか」がコードから伝わります。

「このクラスのインスタンスを、こういう前提で作りたい」
という“作り方のバリエーション”は、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:クラス全体で共有する状態(static プロパティ)
  • getCreatedCount:その状態を外から安全に参照する窓口(static メソッド)

という関係になっています。

「クラスに属する状態」を static プロパティに持たせて、
「それをどう扱うか」を static メソッドに閉じ込めると、
クラスの“クラスとしての顔”がきれいにまとまります。


staticメソッドとインスタンスメソッドの違いを、型から感じる

staticメソッドには this がない(=インスタンス前提ではない)

インスタンスメソッドは、暗黙に this を持ちます。

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

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

greet の中では this.name が使えます。
「このインスタンスの状態」を前提にしたメソッドです。

一方、static メソッドは this をインスタンスとしては使いません。

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

  static helloAll(): void {
    console.log("みなさん、こんにちは");
    // this.name; // こういうのは使えない(this はクラス側を指す)
  }
}
TypeScript

static メソッドの中で this を使うと、それは「クラスそのもの」を指します。
User と同じもの)

なので、「インスタンスの状態を前提にした処理」は static にしてはいけない
というのが大事なポイントです。

型の感覚で言うと、

  • インスタンスメソッド:(this: User, ...) => ... みたいなイメージ
  • static メソッド:(...args) => ...(this なし)

という違いがあります。

呼び出し側の型の違い

呼び出し側から見ると、こういう違いになります。

class Example {
  static staticMethod(): void {}
  instanceMethod(): void {}
}

const e = new Example();

Example.staticMethod(); // OK
// e.staticMethod();    // エラー

e.instanceMethod();     // OK
// Example.instanceMethod(); // エラー
TypeScript

「クラス名から呼ぶか」「インスタンスから呼ぶか」が、
そのまま「static かどうか」の違いです。

コードを読むときも、

  • User.xxx() と書いてあれば「static メソッド」
  • user.xxx() と書いてあれば「インスタンスメソッド」

と意識して見ると、
「これはインスタンス前提の処理かどうか」がすぐ分かるようになります。


実務でのstaticメソッドのよくあるパターン

変換・パース系の処理

例えば、「JSON から User を作る」処理。

type UserJson = {
  name: string;
};

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

  static fromJson(json: UserJson): User {
    return new User(json.name);
  }
}

const json: UserJson = { name: "Taro" };
const u = User.fromJson(json);
TypeScript

fromJson は、

  • まだ User インスタンスがない状態から
  • User インスタンスを作る

という性質の処理なので、static メソッドが自然です。

同じように、

  • fromId(id: number): Promise<User>(ID から取得して作る)
  • fromForm(form: FormData): User(フォーム入力から作る)

など、「何かを元にインスタンスを作る」系は static に寄せると分かりやすくなります。

バリデーション・ユーティリティ

例えば、「メールアドレスとして妥当かどうか」を判定する処理。

class Email {
  static isValid(value: string): boolean {
    return value.includes("@");
  }
}

if (Email.isValid("test@example.com")) {
  // OK
}
TypeScript

ここでも、インスタンスは不要です。
「Email という概念に関するユーティリティ」として static にぶら下げています。

「この処理、わざわざ new しなくてもよくない?」
と感じたら、static メソッドの候補です。


まとめ:staticメソッドを自分の言葉で整理すると

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

  • static メソッドは「インスタンスではなくクラスに属する関数」
  • 呼び出しは User.xxx() のようにクラス名から行う
  • インスタンスの状態(this)に依存しない処理に向いている
  • 代表的な使いどころは
    • インスタンスの“作り方”を表す工場メソッド(fromJson, createAdmin など)
    • クラスに関するユーティリティ(isValid, toSomething など)
    • static プロパティとセットで「クラス全体の状態」を扱う処理
  • 「これは本当に new した“個体”に紐づくべき処理か?」と自問して、
    そうでないなら static を検討する

今あなたが持っているクラスを1つ選んで、
その中のメソッドを眺めながら、

「これ、this 使ってないよな?」
「これ、インスタンスじゃなくてクラスにぶら下げたほうが意味としてきれいじゃない?」

と一つずつ見直してみてください。

その小さな整理が、
クラスを“インスタンスの責務”と“クラスの責務”がきちんと分かれた設計に育てていきます。

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