TypeScriptの「隠蔽」を極める:`private`修飾子と `#` 記号の境界線
プロダクション環境で大規模なフロントエンドアプリケーションを構築していると、必ず突き当たる壁がある。「どこまでを公開し、どこからを隠蔽すべきか」という境界線だ。
TypeScriptの `private` 修飾子を安易に使っていないだろうか? あるいは、JavaScriptのネイティブ機能である `#`(プライベートクラスフィールド)を「新しい書き方だから」という理由だけで採用していないだろうか。
これらは単なる文法の違いではない。「コンパイル時にのみ存在する制約」と「実行時に強制される完全な隠蔽」という、性質の根本が異なる機能だ。この記事では、この二つの使い分けが、なぜあなたのコードの堅牢性と設計の美しさに直結するのかを、コアの視点から紐解く。
—
1. `private`修飾子:コンパイラの「優しい警告」
TypeScriptの `private` は、型チェックの段階でしか機能しない。
class User {
private secretKey: string = “super-secret”;
}
const user = new User();
// @ts-expect-error: コンパイル時にはエラーになるが、実行はできてしまう
console.log(user.secretKey);
// コンパイル後のJSを見ると、secretKeyは普通に存在している
// (user as any).secretKey であれば、実行時にいとも簡単にアクセス可能だ
なぜ `private` を使うのか
`private` の真の価値は、開発者の「うっかりミス」をコンパイル時に防ぐことにある。IDE(VS Codeなど)による補完を抑制し、意図しない外部参照を未然に防ぐための「設計上のガイドライン」だ。
- 利点: 型情報が削除されるため、ランタイム(実行時)のオーバーヘッドが一切ない。
- 用途: チーム開発において、APIインターフェースを意識させ、ドキュメント生成ツール(TypeDoc等)で非公開プロパティを除外したい場合に最適だ。
—
2. `#`プライベートフィールド:ランタイムの「鉄壁の守り」
一方、ECMAScript標準である `#` は、JavaScriptのエンジンレベルで強制される真のプライベートだ。
class SecureToken {
#token: string;
constructor(token: string) {
this.#token = token;
}
get maskedToken() {
return this.#token.slice(0, 4) + ““;
}
}
const instance = new SecureToken(“ABC-123-XYZ”);
// console.log(instance.#token); // 実行時エラー: Private field ‘#token’ must be declared in an enclosing class
なぜ `#` を使うのか
これは「隠蔽」の強度が違う。リフレクションや `Object.keys()` を駆使しても、外部からその値を取り出すことはできない。
- 利点: 外部からの不正な操作をランタイムで完全にシャットアウトできる。
- 注意点: `#` はクラスインスタンスに紐づくため、プロトタイプチェーンの恩恵を受けられない。また、コンパイル時にES2022以降をターゲットにする必要があり、古い環境ではトランスパイルによるクラスのラップが行われるため、わずかなパフォーマンス上のオーバーヘッドが発生する。
—
3. 実務における最適解:どちらをいつ使うべきか
技術選定の指針はシンプルだ。「誰から隠したいのか」を考えること。
推奨される設計パターン:ハイブリッド戦略
フロントエンドのコンポーネント設計や、状態管理クラスでは以下の使い分けを徹底してほしい。
/
- 堅牢なAPIクライアントの例
/
class ApiClient {
// 1. 外部からは絶対にいじらせたくない設定値は ‘#’ で隠蔽する
#baseUrl: string;
// 2. 開発者が型安全に扱いたいメソッドや補助プロパティは ‘private’ を使う
private retryCount: number = 0;
constructor(url: string) {
this.#baseUrl = url;
}
public async fetch(endpoint: string) {
// #baseUrl を使った安全なロジック
return fetch(`${this.#baseUrl}${endpoint}`);
}
// テスト時にのみアクセスしたい場合は、
// あえて private にせず、外部のテストファイルから見えるようにする設計も一考
}
使い分けのチェックリスト
- 「隠蔽」がロジックの安全性(セキュリティ)に直結するか? → `#` を採用せよ。
- 「隠蔽」がただのAPIの使いやすさ(DX)の向上か? → `private` を採用せよ。
- シリアライズが必要か? → `#` フィールドは `JSON.stringify` で無視される。これが必要なら `private` を使った上で、`toJSON` メソッドをオーバーライドする戦略を採れ。
—
まとめ:言語の重みを知る者へ
TypeScriptの `private` は設計のための「言葉」であり、`#` は実行環境に対する「命令」だ。
チーフアーキテクトとして言わせてもらえば、「とりあえずすべてを `private` にする」という思考停止こそが、システムを脆弱にする。
真に堅牢なプロダクトは、コンパイラの静的解析と、ランタイムの実行モデルの双方を理解した上で、意図的に使い分けることで生まれる。あなたの書くコードが、型システムの恩恵を受けつつ、実行時にも揺るぎない保証を持つことを願っている。
さあ、今日のコードからその「境界線」を見直してみてほしい。