readonlyプロパティとは何か(「一度決めたら変えない」約束)
readonly プロパティは、「このプロパティは一度値を入れたら、あとから書き換えてはいけない」という約束を型レベルで表す仕組みです。
TypeScriptでは、オブジェクト型のプロパティに readonly を付けることで、そのプロパティを「読み取り専用」にできます。
type User = {
readonly id: number;
name: string;
};
const user: User = {
id: 1,
name: "Taro",
};
user.name = "Jiro"; // OK
user.id = 2; // エラー: Cannot assign to 'id' because it is a read-only property
TypeScriptここで id は「一度決めたら変えない値」として守られます。
実行時にロックされるわけではなく、コンパイル時に「変えようとしているコード」を止めてくれるのがポイントです。
readonlyプロパティの基本的な書き方
type でのreadonlyプロパティ
type Config = {
readonly env: string;
readonly debug: boolean;
};
const config: Config = {
env: "dev",
debug: true,
};
// config.env = "prod"; // エラー
TypeScriptreadonly env: string は、「env プロパティは string だが、再代入はできない」という意味です。debug も同様に、一度決めたら変えられない設定値として扱われます。
interface でのreadonlyプロパティ
interface User {
readonly id: number;
name: string;
}
const user: User = {
id: 1,
name: "Taro",
};
user.name = "Jiro"; // OK
// user.id = 2; // エラー
TypeScripttype と interface のどちらでも、readonly の書き方と意味は同じです。
「このプロパティは変えちゃダメ」という意図を、型としてはっきり宣言できます。
なぜreadonlyを付けるのか(メリットのイメージ)
「変えてはいけないもの」をコードで守る
ID、作成日時、設定値など、「一度決まったら変わるべきではない値」はたくさんあります。readonly を付けておくと、うっかり書き換えるコードを書いたときに、コンパイル時に止めてくれます。
interface User {
readonly id: number;
name: string;
}
function rename(user: User, newName: string) {
user.name = newName; // OK
// user.id = 999; // エラー(IDを変えようとしている)
}
TypeScript「この関数は名前だけ変えるつもりだったのに、IDまで変えてしまった」
そんなバグを、型が先回りして防いでくれるイメージです。
「この関数はオブジェクトを書き換えません」を型で表現できる
type FooBarBaz = {
foo: number;
bar: number;
baz: number;
};
function sum(obj: Readonly<FooBarBaz>) {
return obj.foo + obj.bar + obj.baz;
}
TypeScriptReadonly<FooBarBaz> を使うと、「この関数は obj の中身を変更しない」ということを型で宣言できます。
呼び出す側も、「この関数に渡してもオブジェクトは壊されない」と安心して使えます。
readonlyは「そのプロパティだけ」に効く(再帰的ではない)
ネストしたオブジェクトの挙動
readonly は、指定したプロパティにだけ効きます。
中にさらにオブジェクトが入っている場合、その中身までは自動では readonly になりません。
type Outer = {
readonly inner: {
value: number;
};
};
let obj: Outer = {
inner: { value: 1 },
};
// obj.inner = { value: 2 }; // エラー: inner は readonly
obj.inner.value = 2; // これはOK
TypeScriptここでは、
innerプロパティ自体は readonly(別のオブジェクトを代入し直すのはNG)- しかし
inner.valueは readonly ではないので、書き換え可能
という状態です。
「中身も全部 readonly にしたい」場合は、Readonly<T> や再帰的なユーティリティ型を使う必要がありますが、
まずは「readonly は“そのプロパティ1段分”だけに効く」と覚えておけば十分です。
readonly配列との関係(さらっと触れておく)
配列にもreadonlyは使える
const list: readonly number[] = [1, 2, 3];
// list.push(4); // エラー: push が存在しない
// list[0] = 9; // エラー: インデックス経由の代入も不可
TypeScriptreadonly number[] は、「この配列は中身を変更してはいけない」という意味です。push や pop のような破壊的メソッドは使えなくなり、要素の再代入も禁止されます。
ただし、map や filter のように「新しい配列を返すメソッド」は問題なく使えます。
「元の配列を壊さない」という設計を、型で強制できるわけです。
初心者がまず掴んでおきたいreadonlyプロパティの感覚
readonlyプロパティの本質は、こうです。
「この値は、作ったときに決めて、それ以降は変えない」という約束を、型として宣言する。
その結果として、
- IDや設定値など「変わるべきでないもの」を守れる
- 関数が「引数のオブジェクトを書き換えるかどうか」を型で表現できる
- うっかりした代入ミスをコンパイル時に潰せる
という状態になります。
「このプロパティ、本当にあとから変わっていい?」
そう自分に問いかけて、「変わってほしくない」と思ったら readonly を付ける。
その小さな判断の積み重ねが、壊れにくくて意図が伝わるコードを作っていきます。
