TypeScriptにおけるクラス開発は、2026年現在、大規模なアプリケーション開発において欠かせない要素となっています。

オブジェクト指向プログラミングの根幹をなす「カプセル化」を実現するための手段として、かつてはprivate修飾子が主流でした。

しかし、現在のモダンな開発現場では、ECMAScriptの標準仕様であるプライベートフィールド(#)の採用が一般的になっています。

本記事では、TypeScriptにおけるプライベートフィールドの基本的な使い方から、従来の修飾子との違い、そして実践的な使い分けの基準について詳しく記述します。

TypeScriptにおけるプライベートフィールド (#) の基礎知識

プライベートフィールドとは、クラスの外部から直接アクセスできないプロパティを定義するためのECMAScript(JavaScript)標準の構文です。

プロパティ名の先頭に#(ハッシュ記号)を付与することで、そのフィールドはそのクラスの内部でのみ参照可能となります。

この機能はTypeScript 3.8から導入されましたが、2026年現在の開発環境ではランタイムのサポートも完全に定着しています。

従来のTypeScript特有のprivate修飾子とは異なり、実行時においても厳格なアクセス制限が保持されることが最大の特徴です。

まずは、基本的な構文を確認してみましょう。

TypeScript
class UserAccount {
  // プライベートフィールドの宣言
  #secretKey: string;

  constructor(key: string) {
    // クラス内部でのみアクセス可能
    this.#secretKey = key;
  }

  getSecretPrefix() {
    // 内部メソッドからは自由に参照できる
    return this.#secretKey.substring(0, 4);
  }
}

const account = new UserAccount("admin-key-12345");
console.log(account.getSecretPrefix());
実行結果
"admi"

上記のコードでは、#secretKeyは外部からアクセスしようとすると構文エラー、あるいは実行時エラーとなります。

これにより、開発者が意図しないデータの書き換えや参照を物理的に防ぐことが可能になります。

private修飾子とプライベートフィールド (#) の決定的な違い

TypeScriptには以前からprivateというアクセス修飾子が存在していましたが、これと#は全く別物です。

private修飾子は、あくまで「TypeScriptのコンパイル時」におけるアクセス制限を提供するものです。

一方でプライベートフィールドは、JavaScriptの言語仕様としてランタイムレベルでの隠蔽を実現します。

以下の表で、主な違いを比較してみましょう。

機能private修飾子プライベートフィールド (#)
チェックのタイミングコンパイル時のみコンパイル時および実行時
実行時の隠蔽なし(通常のプロパティとして存在)あり(外部からアクセス不可)
名前の競合親クラスと同じ名前は不可親クラスと独立して定義可能
宣言の強制省略可能(コンストラクタで定義可)クラスのトップレベルで事前宣言が必須

private修飾子で定義されたフィールドは、コンパイル後のJavaScriptでは単なるパブリックなプロパティに変換されます。

そのため、(obj as any).propertyのように型定義を回避することで、外部からアクセスできてしまうという脆弱性がありました。

プライベートフィールドを使用すれば、このような「ハック」によるアクセスを完全に遮断できます。

ハードプライバシーとソフトプライバシー

技術的には、private修飾子のような挙動を「ソフトプライバシー」、#のような挙動を「ハードプライバシー」と呼びます。

2026年のシステム設計においては、セキュリティや堅牢性が重視されるため、可能な限りハードプライバシーである「#」の利用が推奨されます。

特にライブラリの開発者にとっては、ユーザーが内部実装に依存することを防ぐために非常に有効な手段です。

カプセル化を深める実践的な使い分け

すべてのフィールドを機械的に#にするべきかというと、プロジェクトの設計方針によります。

まず、コンポーネントの状態管理やドメインモデルなど、不変条件(インバリアント)を守る必要がある場所では、プライベートフィールドが最適です。

例えば、バリデーション済みのデータのみを保持したいクラスなどが該当します。

TypeScript
class AgeManager {
  #age: number = 0;

  setAge(value: number) {
    if (value < 0 || value > 150) {
      throw new Error("無効な年齢です。");
    }
    this.#age = value;
  }

  get age() {
    return this.#age;
  }
}

このように、セッターを通じてのみ値を更新できるように強制することで、オブジェクトの状態を常に正しく保つことができます。

一方で、ユニットテストにおいて「テスト用に内部の状態を覗きたい」という要望がある場合、プライベートフィールドは少し扱いづらくなります。

テストコードからも完全にアクセスできないため、パブリックなメソッド経由でしか動作を検証できません。

これは設計としては正しい姿ですが、既存のテスト手法によっては工夫が必要になることもあります。

名前の衝突回避というメリット

プライベートフィールドのもう一つの強力なメリットは、継承関係における名前の衝突を考慮しなくて良い点です。

private修飾子の場合、親クラスと子クラスで同じ名前のプライベート変数を定義することは許可されていません。

しかし、#を使用すれば、各クラスで独立したスコープが保たれるため、同名のプロパティを定義しても安全に動作します。

TypeScript
class Base {
  #id = "base-id";

  printBaseId() {
    console.log(this.#id);
  }
}

class Derived extends Base {
  #id = "derived-id";

  printDerivedId() {
    console.log(this.#id);
  }
}

const instance = new Derived();
instance.printBaseId();
instance.printDerivedId();
実行結果
"base-id"
"derived-id"

このように、継承先で意図せず親クラスの内部状態を上書きしてしまうリスクを排除できるため、フレームワークやベースクラスを設計する際に非常に有用です。

継承クラスにおける挙動の理解

プライベートフィールドは、継承した子クラスからも直接参照することはできません。

これはprivate修飾子と同様ですが、より厳格に守られています。

もし子クラスからアクセスしたいプロパティがある場合は、protected修飾子を使用するか、ゲッター/セッターを定義する必要があります。

TypeScriptにおいてprotectedに相当する「ランタイムレベルの隠蔽」を行う標準構文は現在のところ存在しません。

したがって、「完全に隠すなら#」「継承を許可するならprotected」という明確なルールで使い分けることがベストプラクティスとなります。

2026年におけるブラウザ・ランタイムの互換性とパフォーマンス

2026年現在、主要なブラウザ(Chrome, Edge, Firefox, Safari)およびNode.jsやDeno, Bunなどのランタイム環境では、プライベートフィールドがネイティブで完全にサポートされています。

数年前までは、古いブラウザをサポートするためにトランスパイルを行う際、WeakMapを使用した複雑なコードに変換され、実行速度が低下することが懸念されていました。

しかし現在は、ほとんどの環境でネイティブ実装が利用できるため、パフォーマンス上のオーバーヘッドを気にする必要はほぼなくなっています。

ただし、非常に大量のオブジェクトを生成するようなケースでは、通常のプロパティアクセスよりも微差ながらメモリ使用量やアクセス速度に影響が出る場合があります。

通常の業務アプリケーション開発においては無視できるレベルですが、パフォーマンスがクリティカルなライブラリ開発では留意しておくと良いでしょう。

開発における注意点とよくあるエラー

プライベートフィールドを導入する際に遭遇しやすいエラーについても触れておきます。

もっとも多いのは、フィールドの事前宣言忘れです。

JavaScript/TypeScriptの通常のプロパティは、コンストラクタ内でいきなりthis.prop = valueと書くことができますが、#はクラスの直下で宣言されていなければなりません。

TypeScript
class ErrorExample {
  constructor() {
    // 宣言がないためエラーになる
    // this.#data = 10; 
  }
}

また、in演算子を使用してプライベートフィールドの存在を確認することも可能ですが、少し特殊な構文になります。

#field in objectという形式で、そのオブジェクトが指定したプライベートフィールドを持っているか判定できます。

これは、同じクラスの別のインスタンスが渡されたときに、安全にアクセスできるかを確認する場合などに活用されます。

TypeScript
class Validator {
  #secret = "validated";

  static isValidator(obj: any): obj is Validator {
    // インスタンスが#secretを持っているかチェック
    return #secret in obj;
  }
}

このように、強力な隠蔽能力を持ちながらも、型安全性を維持するための仕組みが整っています。

まとめ

TypeScriptのプライベートフィールド(#)は、真のカプセル化を実現し、堅牢なコードを構築するための強力な武器です。

従来のprivate修飾子とは異なり、コンパイル後のJavaScriptでもデータが保護されるため、意図しないアクセスや名前の衝突を完全に防ぐことができます。

2026年のモダンな開発においては、基本的にはプライベートフィールド(#)を優先的に使用し、継承が必要な場合のみprotectedを選択するという方針が推奨されます。

この機能を正しく使いこなすことで、大規模化するプロジェクトにおいても変更に強く、メンテナンス性の高いクラス設計を実現できるでしょう。

ぜひ日々のコーディングにおいて、プライベートフィールドを積極的に活用し、一歩進んだカプセル化を実践してみてください。