TypeScriptの「壁」を解剖する:`private`修飾子と `#` プライベートフィールドの深淵
TypeScriptの設計思想において、型システムは「静的な幻想」であり、ランタイムは「冷酷な現実」である。この二つのレイヤーの乖離を理解せずして、堅牢なアーキテクチャを構築することは不可能だ。
今回は、多くのシニアエンジニアが混同しがちな「`private` 修飾子」と「JavaScriptの `#` プライベートクラスフィールド」の境界線について、コンパイラの振る舞いとランタイムのメモリモデルという観点から深掘りする。
—
1. `private` 修飾子:型システムによる「気休め」
TypeScriptの `private` は、コンパイル時にのみ存在する「設計上の境界線」だ。
class Fortress {
private secretKey: string = “SUPER_SECRET_123”;
}
const fort = new Fortress();
// コンパイルエラー: Property ‘secretKey’ is private and only accessible within class ‘Fortress’.
console.log(fort.secretKey);
// しかし、型キャストによってその防壁は霧散する
console.log((fort as any).secretKey); // “SUPER_SECRET_123”
コンパイラの挙動と限界
`tsc` は、ソースコードを解析して AST(抽象構文木)を構築する際、`private` アクセス修飾子を単なるメタデータとして処理する。生成される JavaScript コードからは、`private` という文字は完全に抹消される。
つまり、ランタイム(V8やSpiderMonkey)から見れば、`secretKey` は単なるパブリックなプロパティに過ぎない。リフレクションやプロトタイプ汚染、あるいは単なる `any` キャストによって、この防壁は容易に突破される。「型システムが保証しているのは、開発時の静的解析における整合性だけである」という事実を忘れてはならない。
—
2. `#` プライベートクラスフィールド:ランタイムによる「物理障壁」
ECMAScript 2022 で導入された `#` プライベートフィールドは、もはや型システムの話ではない。これは V8 等のランタイムエンジンが実装する「隠蔽」である。
class SecureVault {
#internalBuffer: Uint8Array;
constructor() {
this.#internalBuffer = new Uint8Array([0x01, 0x02]);
}
get buffer() {
return this.#internalBuffer;
}
}
const vault = new SecureVault();
// コンパイルエラー以前に、ランタイムで SyntaxError を引き起こす
// console.log(vault.#internalBuffer);
メモリ最適化とスコープの分離
`#` が付与されたプロパティは、オブジェクトのプロパティリスト(`[[OwnPropertyKeys]]`)にすら現れない。ランタイムは、このプロパティをオブジェクトインスタンスの外部にある「隠しスロット(WeakMapのような構造)」にバインドする。
- カプセル化の真実: インスタンスを `Object.keys()` で走査しても、`JSON.stringify()` しても、内部プロパティは一切露出しない。
- パフォーマンスへの影響: `#` フィールドはコンパイラが自動的に最適化を行い、プロパティルックアップのコストを定数時間(O(1))に抑えるための特殊なスロットアクセスとして処理される。
—
3. 防壁を突破する攻撃者への回答
セキュリティ研究者の視点から言えば、TypeScriptの `private` は「プログラマのミスを防ぐガードレール」であり、`#` は「実行時メモリを保護するシェルター」である。
なぜ `private` を使い続けるのか?
もし `#` が真のプライベートであるなら、なぜ我々は `private` を使い続けるのか。それは 「依存関係の注入とテストの容易性」 にある。
DI コンテナや Mocking ライブラリは、ランタイムの隠蔽を強引に突破してオブジェクトを改変することがある。`#` を多用すると、Jest の `spyOn` やプロキシを用いたテストが極めて困難になる。
// 推奨されるハイブリッド戦略
class Service {
// テスト時はアクセス可能にしたい(型レベルの制限)
private _dependency: Dependency;
// 外部からの直接アクセスを物理的に遮断したい(ランタイムレベルの制限)
#secret: string;
constructor(dep: Dependency) {
this._dependency = dep;
this.#secret = “HIDDEN_DATA”;
}
}
—
4. 結び:アーキテクトとしての矜持
TypeScriptにおけるアクセス制御の選定基準は、以下の二点に集約される。
1. 開発体験と保守性(`private`): チーム開発において、型安全なインターフェースを提供し、依存の方向性を強制する。これは「規約」である。
2. 不変性とランタイムの安全性(`#`): シリアライズされるデータ、あるいはクラス内部のメモリ管理において、外部からの不当な書き換えを物理的に防ぐ必要がある場合。これは「防御」である。
我々が書くコードは、単なるテキストではない。それはメモリ上に配置され、イベントループを駆け巡り、V8の最適化パイプラインを通る「命令」である。
型システムという「虚構」を深く理解し、ランタイムという「現実」を制御する。その境地に至った者だけが、真に堅牢なアーキテクチャを設計できるのだ。
コードは嘘をつかない。しかし、コンパイラは時として、我々が望む以上に親切な嘘をつく。その裏側にある真実を、常に読み解いていくことだ。