境界の欺瞞と真実:TypeScriptの`private`修飾子とJavaScriptの`#`プライベートフィールドの深淵
TypeScriptを「単なるJavaScriptの型補完ツール」と捉えているのであれば、それは巨大なシステムの設計を誤る第一歩だ。我々が扱うのはコンパイラによる静的解析の世界と、V8やSpiderMonkeyが実行する動的メモリの世界、その二つの交差点である。
今日は、多くのエンジニアが「なんとなく」使い分けている`private`修飾子と、JS標準の`#`(Private Class Fields)が、コンパイル時とランタイムにおいてどう異なるのか、その防壁の強度を紐解いていく。
—
1. 静的解析の「幻想」:`private`修飾子の正体
TypeScriptの`private`は、コンパイル時のみ存在するメタデータに過ぎない。
class SecureVault {
private secret: string = “super-secret-key”;
}
const vault = new SecureVault();
// コンパイルエラー: プロパティ ‘secret’ は private であり…
// console.log(vault.secret);
// しかし、型アサーションで防壁は崩壊する
console.log((vault as any).secret); // 実行時: “super-secret-key”
この挙動は、TypeScriptが設計思想として「JSの実行時のパフォーマンスを一切損なわない」ことを優先しているからだ。トランスパイル後のJavaScriptを見ればわかるが、`private`は跡形もなく消え去り、ただのプロパティアクセスに変換される。
これは「開発中の誤用を防ぐ」という点では有用だが、実行時のメモリ保護やデータ秘匿の観点では無力である。 リフレクションや外部モジュールからのアクセスに対して、何の制約も課さない。
—
2. ランタイムの「実体」:`#`プライベートフィールド
対して、ECMAScript標準である`#`(Private Class Fields)は、V8などのランタイムエンジンがネイティブでサポートする仕組みだ。これは単なる命名規則ではない。
コンパイラの裏側とメモリレイアウト
`#`を使用すると、そのプロパティはインスタンスのハッシュマップ(通常のオブジェクトの`[[Properties]]`)には保存されない。代わりに、エンジン内部のプライベート名前空間(Private Brand)に紐付けられ、クラスのスコープ外からは一切のアクセスが物理的に遮断される。
class HardenedVault {
#secret: string = “AES-256-GCM”;
reveal() {
return this.#secret;
}
}
const h = new HardenedVault();
// console.log(h.#secret); // SyntaxError: 不正なプライベートメンバアクセス
// (h as any).#secret // コンパイルエラー: 静的解析で弾かれる
ここでのポイントは、`#`はコンパイル後に`WeakMap`を用いたエミュレーション、あるいは近年のエンジンではネイティブのプライベートスロットとして保持される点だ。これにより、外部からプロパティを「注入」したり「閲覧」したりすることは不可能に近い。
—
3. インターフェース(Interface)との相克
ここで重要な問いが生じる。「なぜInterfaceにはプライベートメンバーを含められないのか?」という点だ。
結論から言えば、Interfaceは「公開APIの契約(Contract)」である。プライベートな実装詳細がインターフェースに含まれることは、カプセル化の原則に反する。
もしあなたがライブラリ設計者なら、以下の指針を胸に刻んでほしい。
- 公開APIの契約: `interface` を使い、純粋な振る舞いのみを定義せよ。
- 実装の隠蔽: `class` の内部で `#` を使用し、実装詳細が外部に漏れないようメモリレベルでロックせよ。
実践的アーキテクチャ:防壁の構築
シニアレベルの設計では、インターフェースで公開範囲を制御し、クラスの内部で「真の隠蔽」を行う。
interface Vault {
reveal: () => string;
}
class SecretContainer implements Vault {
#secret: string = “Deep-Internal-State”;
// インターフェースを満たしつつ、#secretの露出を阻止する
public reveal(): string {
return `Access granted: ${this.#secret}`;
}
}
const myVault: Vault = new SecretContainer();
// myVault.#secret は存在すら認識できない
—
4. チーフアーキテクトからの忠告
エンジニアがセキュリティを語る際、往々にして「型があれば安心」という慢心に陥る。しかし、型はあくまで開発者のための地図であり、実行時の城壁ではない。
- `private`修飾子: 開発中の型安全性を高め、リファクタリングを容易にするための「警告灯」。
- `#`プライベート: プロトタイプ汚染や外部からの不正なプロパティ改竄を防ぐための「物理的遮断」。
もし君がNode.jsのランタイムや、秘匿性の高いミドルウェアを書いているのであれば、迷わず `#` を採用すべきだ。コンパイル時の型定義に満足せず、実行時に何が起きるか、メモリのどこにデータが保持されるかを常に意識する。
それが、TypeScriptを掌握するということだ。コードは単なる記号の羅列ではない。それは、マシンに対する厳格な命令であり、同時に防御の要塞でもあるのだから。