【実務・中級編】Interfaceの「プライベートプロパティ」問題と、#記号による真のプライベート化の比較 – TypeScript コア・型システムの基礎解析バイブル

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` を見かけたらこう問いかけてみてほしい。「これは本当に隠蔽が必要か? それとも単なる警告でいいのか?」と。その問いこそが、プロダクトをより堅牢なものに変えていく。

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