Menu

TypeScriptのアクセス修飾子: public・private・protected

TypeScriptには public、private、protected の3つのアクセス修飾子と readonly があります。それぞれが何を許可するか、TypeScriptの private がコンパイル時のチェックであるのに対し、JavaScriptの #private フィールドが実行時に強制される理由、どちらを選ぶべきかを解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

TypeScriptにはクラスのメンバー用のアクセス修飾子が3つあります。public(デフォルト)、protected、private です。メンバーをどこで使えるかを制御し、間違った場所からのアクセスはコンパイラーが報告します。

最後の行はコンパイルエラーですが、それでも実行したときの出力を見てください: 123-45-6789。これがこのページでいちばん大事な事実で、private のセクションでその理由を説明します。

各修飾子が許可すること

修飾子クラスの中サブクラスの中外から実行時に強制されるか
public(デフォルト)はいはいはい強制するものがない
protectedはいはいいいえいいえ
privateはいいいえいいえいいえ
#name(JavaScript)はいいいえいいえはい
readonly読み取り、コンストラクターでは書き込みも読み取り読み取りいいえ

修飾子はフィールド、メソッド、getter と setter、コンストラクター、引数プロパティ(constructor(private id: string))に使えます。public は書かなくてもよく、多くのコードベースでは省略しています。

private はコンパイル時のチェック

TypeScriptは private をほかの型と一緒に消去します。コンパイルされたクラスには普通のプロパティがあるので、型チェッカーを通らないものからは見えます。素のJavaScriptの呼び出し側、JSON.stringify、Object.keys、そして意図的な抜け道として許可されているTypeScript自身のブラケット記法です。

これは private の本来の目的、つまりメンバーが実装の詳細であることをほかの開発者(とエディター)に伝えるという点では問題ありません。セキュリティの境界ではないので、自分で取り除かない限り、private フィールドはログやJSONのレスポンスに含まれてしまいます。

#private フィールド: 実行時に強制される

JavaScriptには # で書く独自のプライベートフィールドがあります。これはエンジンが強制します。クラスの外では obj.#field は構文としてすら正しくなく、フィールドは Object.keys、JSON.stringify、console.log にも現れません。

クラスの外で s.#token と書くとエラー TS18013(Property '#token' is not accessible outside class 'Session' because it has a private identifier)になり、private の場合と違って実行時にも回避する方法はありません。実行時のルールについては JavaScriptのプライベートフィールド を参照してください。

private と #private のどちらを使うか

private x#x
チェックするものコンパイラーJavaScriptエンジン
Object.keys / JSON.stringify から見えるか見える見えない
ブラケット記法 obj["x"]可能不可能
サブクラスが自分の x を宣言できるかできない(衝突する)できる(クラスごとに別の #x)
スプレッド { ...obj } でコピーされるかされるされない
メソッドの構文private helper()#helper()

実行時にもデータをプライベートに保つ必要があるとき(トークン、ライブラリの利用者に触らせたくない内部状態)や、JSON.stringify に含めたくないときは #private を使います。きれいな公開APIだけが目的のとき、フレームワークがリフレクションでフィールドを読む必要があるとき、既存のコードベースのスタイルに合わせるときは private を使います。両方を組み合わせてはいけません。private #x はエラー TS18010(An accessibility modifier cannot be used with a private identifier)です。

protected とサブクラス

protected メンバーはクラスの中と、それを継承するクラスの中で使えますが、外からインスタンスを通して使うことはできません。

意外なルールが1つあります。Polygon の中では、this や別の Polygon の sides は読めますが、引数として渡された普通の Shape の sides は読めません。other: Shape に対する other.sides はエラー TS2446(Property 'sides' is protected and only accessible through an instance of class 'Polygon'. This is an instance of class 'Shape'.)です。サブクラスが protected メンバーにアクセスできるのは、階層の中で自分の系統に属するオブジェクトだけです。

サブクラスは protected メンバーを再宣言して public にできますが、public メンバーを protected や private にはできません。再宣言するときは初期値付き(public override sides = 6)か、型だけ(declare public sides: number)で書きます。単なる public sides: number; は TS2564 と TS2612 で拒否されます。現在のクラスフィールドの仕様では、super() が戻ったあとに継承した値を undefined にリセットしてしまうからです。

readonly

readonly は、インスタンスの作成後の再代入を禁止します。フィールドは宣言時かコンストラクターの中で設定でき、それ以降の代入はエラー TS2540 です。

ここには2つの限界が表れています。readonly は浅く、配列を置き換えることはできませんが、中身は変更できます(push を禁止するには readonly string[] 型にします)。そして private と同じく実行時には消えるので、コンパイラーが拒否した代入も実行されてしまいます。実行時に変更されてはいけない値には、Object.freeze か setter のない getter を使いましょう。Readonly<T> と readonly 配列については readonly のページで扱います。

readonly はアクセス修飾子と組み合わせられます。private readonly cache = new Map<string, number>() は、内部で使い、再代入しないフィールドのよくあるパターンです。

よくある間違い

  • private をセキュリティとして扱う。 実行時には消えています。隠さなければならないデータには #field を使い、秘密を含むオブジェクトをそのまま JSON.stringify に渡してはいけません。
  • あらゆる場所に public を書く。 デフォルトなので、書いても何も変わりません。
  • 「内部用」のものすべてに protected を使う。 必要とするサブクラスがないなら、private のほうが公開範囲を小さく保てます。
  • readonly がネストしたデータを凍結すると期待する。 止めるのはプロパティそのものの再代入だけです。

よくある質問

TypeScriptのアクセス修飾子にはどんなものがありますか?

public(デフォルト。どこからでもアクセスできる)、protected(クラスとそのサブクラスの中)、private(クラスの中だけ)です。readonly は別の修飾子で、コンストラクターのあとの再代入を禁止し、3つのどれとも組み合わせられます。

TypeScriptの private と #private の違いは何ですか?

private はコンパイラーがチェックするだけで、出力されるJavaScriptでは普通のプロパティなので、obj["secret"]、JSON.stringify、Object.keys からは見えます。#secret はJavaScriptのプライベートフィールドで、実行環境が強制し、クラスの外のコードからはまったく読めません。

TypeScriptの private は本当にプライベートですか?

コンパイル時だけです。コンパイル後のフィールドは普通のプロパティで、どんなJavaScriptのコードからも読めます。TypeScriptは抜け道として、private メンバーへのブラケット記法のアクセス(obj["field"])さえ許可しています。実行時にもプライベートにする必要があるなら #field を使いましょう。

TypeScriptの protected と private の違いは何ですか?

private メンバーは宣言したクラスの中からしか見えません。protected メンバーはサブクラスの中からも見えます。どちらも、クラス階層の外からインスタンスを通してアクセスすることはできません。

TypeScriptで readonly プロパティは変更できますか?

readonly プロパティは宣言時かコンストラクターの中でだけ代入でき、それ以外の場所ではできません(TS2540 になります)。readonly は浅いので、readonly tags: string[] を置き換えることはできませんが、tags.push() は動きます。それも禁止するには readonly string[] を使います。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める