readonly は、オブジェクトを作るときに一度だけ設定でき、再代入できないプロパティを示します。Readonly<T> はそれを型のすべてのプロパティに適用し、readonly T[] は配列に同じことをします:
最後の行は、readonly についていちばん大事な事実を示しています。コンパイラーがチェックするもので、実行時に強制されるものではありません。代入はコンパイルエラーでした(ここでは @ts-expect-error で抑えています)が、出力されたJavaScriptはそれでも実行しました。抑制がなければファイルはコンパイルできず、readonly が役目を果たすのはそこです。
readonly プロパティ
インターフェース、型リテラル、クラスで、プロパティ名の前に readonly を付けます。プロパティは初期化できますが、再代入はできません:
クラスでは、readonly のフィールドは宣言時かコンストラクターの中でだけ代入でき、それ以外の場所ではできません。いちばん短い書き方はパラメータープロパティ constructor(readonly id: string) {} で、フィールドの宣言と代入を一度に行います。フィールドとコンストラクター全般については クラスのページ で扱っています。
Readonly<T>: すべてのプロパティを一度に
Readonly<T> は、T のすべてのプロパティを readonly にするユーティリティ型です。アプリケーションの状態のように、受け渡しはするが変更してはいけない値に便利です:
関数のシグネチャが、addItem は古い状態を変更するのではなく新しい状態を返すことを読む人に伝え、コンパイラーは関数にその約束を守らせます。Readonly<T> はマップ型 { readonly [P in keyof T]: T[P] } として定義されています。
readonly 配列: readonly T[] と ReadonlyArray<T>
readonly number[] と ReadonlyArray<number> は同じ型です。変更するメソッド(push、pop、shift、splice、sort、reverse、fill など)をすべて取り除き、インデックスへの代入を禁止します。変更しないメソッドは残り、普通の配列を返します:
引数として readonly T[] を受け取ることは、呼び出し側の配列を変更しないという約束です。つまずきやすいのは逆方向です。readonly 配列は、普通の T[] を受け取る関数には渡せません。その関数が配列を変更するかもしれないからです。
index.ts(7,17): error TS4104: The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
sum は何も変更しないので、readonly number[] を受け取るように変えれば直ります。配列を読むだけの関数は常に readonly の型を受け取るようにしましょう。そうすれば両方の種類を受け付けられます。自分が管理していない関数なら、コピーを渡します: sum([...prices])。
ReadonlyMap と ReadonlySet
Map と Set にも readonly 版があります。ReadonlyMap<K, V> は get、has、size、forEach とイテレーターを持ちますが、set、delete、clear はありません。ReadonlySet<T> には add、delete、clear がありません:
クラスは、private な変更可能な Map を持ち、ReadonlyMap と型付けした getter で公開することがよくあります。外のコードはデータを読めますが、その参照を通して変更することはできません。
readonly は浅い
readonly と Readonly<T> が守るのはプロパティそのものだけで、それが指すオブジェクトや配列は守りません:
DeepReadonly<T> はネストしたすべてのオブジェクト型に自分自身を適用し、配列の型に対するマップ型は readonly 配列を作るので、members は readonly string[] になります。それでもこれは型のレベルの約束で、実行時の保護ではありません。
コンパイル時だけ: 別の参照を通した変更
readonly の型が制御するのは、1つの参照ができることです。同じオブジェクトへの、readonly なしで型付けされた別の参照からは変更でき、TypeScriptは readonly の型を変更可能な型に代入することさえ許します:
mutable = settings という代入がコンパイルできるのは、2つのオブジェクト型が互換かをチェックするとき、TypeScriptが readonly プロパティを考慮しないからです。TypeScriptのハンドブックはこのことをはっきり書いていて、そのため readonly プロパティは別名を通して変わりうると注意しています。readonly 配列は違います。上の TS4104 エラーがまさにそのチェックです。Object.freeze は実行時の変更を本当に防ぎます。出力されるコードは strict モードで動き、凍結されたプロパティに書き込むと TypeError が投げられます。readonly と同じく、Object.freeze も浅いものです。
readonly、const、as const、Object.freeze
リテラルに as const を付けると、すべての階層ですべてのプロパティが readonly になり、リテラル型も保たれます。深く readonly な値を得るいちばん簡単な方法であることが多いです:
const theme = { mode: "dark", sizes: [12, 14] } as const;
// { readonly mode: "dark"; readonly sizes: readonly [12, 14] }
| 対象 | 深く働くか | 実行時の効果 | 例 | |
|---|---|---|---|---|
const | 変数の束縛 | いいえ | 変数を再代入できない | const user = {...} |
readonly | 1つのプロパティまたは配列の型 | いいえ | なし | readonly id: string |
Readonly<T> | 型のすべてのプロパティ | いいえ | なし | Readonly<State> |
as const | リテラルの式 | はい | なし | { ... } as const |
Object.freeze | オブジェクトの値 | いいえ | 書き込みが失敗する(strict モードでは例外) | Object.freeze(obj) |
const と readonly は別の疑問に答えます。const は名前が別のものを指すのを止め、readonly はプロパティが変わるのを止めます。const のオブジェクトのプロパティも、readonly でなければ再代入できます。
よくある質問
TypeScriptの readonly は何をしますか?
readonly は、オブジェクトを作るとき(またはクラスのコンストラクターの中)には設定できるが、そのあとは再代入できないプロパティを示します。あとで代入するとコンパイルエラー TS2540 になります。型のチェックにすぎず、出力されるJavaScriptには何の保護も含まれません。
TypeScriptの readonly と const の違いは何ですか?
const は変数についてのものです。名前を別の値に向けることはできませんが、持っているオブジェクトは変更できます。readonly はプロパティについてのもので、そのプロパティを再代入できません。const user = { name: "Ada" } では user.name = "x" ができますが、readonly name プロパティではできません。
TypeScriptで配列を readonly にするには?
readonly T[] または ReadonlyArray<T>(同じ型です)と型注釈を付けます。push、pop、sort、splice のような変更するメソッドが型からなくなり、インデックスへの代入はエラーになります。map、filter、slice のような変更しないメソッドは引き続き使え、普通の配列を返します。
TypeScriptの Readonly は深く働きますか?
いいえ。Readonly<T> と readonly が守るのはトップレベルのプロパティだけで、中のネストしたオブジェクトや配列は変更できます。型のレベルで深く守るには、リテラルに as const を付けるか、再帰的な DeepReadonly<T> 型を書きます。
readonly は実行時の変更を防ぎますか?
防ぎません。型は消去されるので、readonly プロパティは実行時には普通のプロパティで、同じオブジェクトへの変更可能な参照を持つコード(や素のJavaScript)からは変更できます。実行時の保護が必要なら Object.freeze を使いましょう。TypeScriptはその結果を Readonly<T> と型付けします。