TypeScriptの「隠蔽」を掌握せよ:Interface、private修飾子、そして真のプライベート「#」の境界線
TypeScriptを扱うエンジニアの多くが、一度は直面する壁がある。「どうすればこのプロパティを外部から隠せるか?」という問いだ。
多くの初学者は `private` 修飾子に飛びつく。しかし、それがコンパイル時のみの「幻想」であり、実行時のJavaScriptでは裸同然であるという事実を理解していない場合、プロダクション環境では致命的なバグの温床となる。
今日は、TypeScriptの型システムとJavaScriptの実行時プライベートが交差するこの境界線を、アーキテクトの視点から解剖する。
—
1. 「private」は型システムの約束事であり、壁ではない
TypeScriptの `private` は、コンパイル時のみに作用する「警告」だ。
class User {
private secretKey: string = “super-secret”;
}
const user = new User();
// @ts-expect-error: コンパイル時には怒られる
console.log(user.secretKey);
// しかし、こうすれば簡単にアクセスできる(実行時はただのプロパティ)
console.log((user as any).secretKey);
実務でこれを「セキュリティ」の境界線だと思ってはいけない。`private` はあくまで「開発体験(DX)」と「設計の意図」をコンパイラに伝えるためのメタデータだ。Interfaceによる契約も同様で、Interfaceはコンパイル後に消滅する。
2. 真のプライベート「#」による物理的遮断
JavaScript(ES2022)で導入された `#` 記法は、単なるメタデータではない。V8エンジンレベルでの物理的な隠蔽だ。
class SecureData {
#internalValue: string;
constructor(val: string) {
this.#internalValue = val;
}
public getPublicValue() {
return this.#internalValue.slice(0, 3) + “”;
}
}
const data = new SecureData(“my-password”);
// TypeScriptはエラーを出し、実行環境(JS)でもプロパティが存在しないかのように振る舞う
// console.log(data.#internalValue); // Syntax Error
この `#` は `Object.keys()` や `JSON.stringify()` にも列挙されず、プロトタイプチェーンの外側に位置する。堅牢なライブラリ設計や、機密性を保つべきコンポーネントの状態管理には、これを使うのが現代の最適解だ。
—
3. 実務で「Interface」とどう共存させるか
ここで重要なのは、「Interfaceにプライベートメンバーは記述できない」というTypeScriptの言語仕様だ。
Interfaceは「公開される契約」の定義である。一方で `#` を持つクラスは「実装の隠蔽」を目的とする。この2つを組み合わせた、最も保守性の高い設計パターンを提示する。
推奨される設計パターン:Facadeパターン
APIクライアントや複雑な状態管理クラスを作る際、Interfaceで振る舞いを定義し、内部実装は `#` で隠す。
// 1. 公開するコントラクト(Interface)
interface IApiClient {
fetchData: () => Promise
}
// 2. 実装クラス(内部は # で堅牢に)
class ApiClient implements IApiClient {
#token: string; // 外部からは絶対に触れない
constructor(token: string) {
this.#token = token;
}
async fetchData(): Promise
// 内部実装で #token を使用
return fetch(‘/api/data’, {
headers: { Authorization: `Bearer ${this.#token}` }
}).then(res => res.text());
}
}
// 利用側:Interface越しに操作する
const client: IApiClient = new ApiClient(“secret-token”);
client.fetchData();
// client.#token は存在すら見えない(IDEにも表示されない)
なぜこの設計が美しいのか?
1. カプセル化の徹底: 利用側は `#token` の存在を知る必要がない。IDEのサジェストにも出てこないため、実装者が誤って触るリスクがゼロになる。
2. テスト容易性: `IApiClient` をモックすれば、テスト時に `#` の存在を気にする必要がない。
3. パフォーマンス: `#` プロパティは、通常のプロパティアクセスよりも最適化が効きやすいケースがある。V8エンジンの最適化パスにおいて、プライベートフィールドはコンパイラが「外部から変更されない」ことを保証できるためだ。
—
結論:迷ったらこう選べ
- 単なる「設計意図」をチームに伝えたいだけなら: `private` 修飾子で十分。
- APIの隠蔽や、実行時のデータ保護(機密情報など)が必要なら: `#` を使う。
- DI(依存性の注入)やコンポーネント設計: インターフェースには公開すべきメソッドのみを書き、実装クラスの内部は `#` で閉じる。
「型は嘘をつかない」というのがTypeScriptの美学だが、「実行時のメモリは嘘をつく」。この冷徹な現実を理解した上で、TypeScriptの型システムとJavaScriptの実行機能の「いいとこ取り」をするのが、真のシニアエンジニアの流儀だ。
明日からのコードレビューでは、`private` を見かけたらこう問いかけてみてほしい。「これは本当に隠蔽が必要か? それとも単なる警告でいいのか?」と。その問いこそが、プロダクトをより堅牢なものに変えていく。