ゴール:「この値は“変わらない”」を型で約束できるようになる
readonly プロパティは、
クラス設計でめちゃくちゃコスパのいい武器です。
一言でいうと、
「この値は、作ったあとに書き換えちゃダメ」を
コンパイラに見張らせる仕組み
です。
「変わらない前提の値」と「変わる前提の値」をちゃんと分けられると、
クラスの意味が一気にクリアになります。
順番に、かみ砕いていきます。
基本:readonlyプロパティとは何か
「代入できるタイミングが“コンストラクタの中だけ”になる」
まずは一番シンプルな例から。
class User {
readonly id: number;
name: string;
constructor(id: number, name: string) {
this.id = id; // OK(コンストラクタ内)
this.name = name;
}
changeName(newName: string) {
this.name = newName; // OK
// this.id = 999; // エラー:readonly はここでは再代入不可
}
}
const u = new User(1, "Taro");
// u.id = 2; // エラー:外からも再代入不可
u.name = "Jiro"; // OK
TypeScriptreadonly が付いていると、
- コンストラクタの中では代入できる
- それ以外の場所(メソッド内・外部コード)では再代入できない
というルールになります。
ここで重要なのは、
「このプロパティは“生成時に決まって、その後は変わらない”」
という前提を、型で表現している
ということです。
コンストラクタ省略記法+readonly
TypeScript のお約束の省略記法とも相性がいいです。
class User {
constructor(
public readonly id: number,
public name: string
) {}
}
const u = new User(1, "Taro");
// u.id = 2; // エラー
u.name = "Jiro"; // OK
TypeScriptpublic readonly id: number で、
- プロパティ宣言
- コンストラクタ引数
this.id = idの代入- readonly 指定
を一気にやってくれます。
どんなプロパティにreadonlyを付けるべきか
「ビジネス的に変わったら困るもの」
まず真っ先に候補になるのは、
「変わったら意味が破綻するもの」です。
例えば:
- ID(ユーザーID、注文IDなど)
- 作成日時(createdAt)
- 生成時に決まる一意なトークン
- 「このインスタンスの“性質”」を表すフラグ
など。
class Order {
constructor(
public readonly id: number,
public readonly createdAt: Date,
public status: "pending" | "paid" | "shipped"
) {}
}
const o = new Order(1, new Date(), "pending");
// o.id = 2; // エラー
// o.createdAt = ... // エラー
o.status = "paid"; // OK
TypeScriptここでの設計はこうです。
idとcreatedAtは「一度決まったら変わらない」statusは「状態として変わりうる」
この区別を readonly でハッキリさせることで、
「何が変わってよくて、何が変わっちゃダメか」がコードから伝わります。
「変えられるとバグの温床になるもの」
もう一歩踏み込むと、
「変えられても技術的には動くけど、
変えられるとバグの温床になるもの」
も readonly 候補です。
例えば、内部的に使う「設定値」や「定数的な情報」。
class Pagination {
constructor(
public readonly pageSize: number,
public currentPage: number = 1
) {}
next() {
this.currentPage += 1;
}
}
TypeScriptpageSize を途中で変えられると、
「どのページからどの件数を取るのか」が一気に怪しくなります。
「この値、途中で変えられたらロジックが破綻するな」と感じたら、
迷わず readonly を付けていいです。
readonlyと「不変オブジェクト」の感覚
プロパティ自体はreadonlyでも、中身は変えられることがある
ここで一つ、よくハマるポイントがあります。
class Team {
readonly members: string[];
constructor(members: string[]) {
this.members = members;
}
}
const t = new Team(["Taro", "Hanako"]);
t.members.push("Jiro"); // これはOK
TypeScriptmembers プロパティは readonly ですが、
その中身(配列の要素)は変えられます。
readonly が守っているのは「プロパティへの再代入」であって、
「中身のオブジェクトの変更」ではないからです。
つまり、
t.members = []はダメ(プロパティの再代入)t.members.push(...)はOK(中身の変更)
ということになります。
「中身も含めて変えさせたくない」なら、別の工夫が必要
本当に「中身も含めて変えさせたくない」なら、
いくつか選択肢があります。
例えば、コピーを返す:
class Team {
private readonly _members: string[];
constructor(members: string[]) {
this._members = [...members];
}
get members(): string[] {
return [...this._members]; // コピーを返す
}
}
const t = new Team(["Taro", "Hanako"]);
const ms = t.members;
ms.push("Jiro"); // Team 内部の配列は変わらない
TypeScriptここでは、
- 内部の配列
_membersはprivate readonly - 外からは
get members()でコピーだけ見せる
という設計にしています。
「この配列、外からいじられたら絶対困る」というときは、readonly だけでなく、
「中身を直接触らせない」という設計もセットで考えるといいです。
readonlyとコンストラクタ設計のセット使い
「生成時に必須+その後不変」の黄金パターン
readonly が一番気持ちよくハマるのは、
「コンストラクタで必ず渡させる」+「その後は変えさせない」
という組み合わせです。
class User {
constructor(
public readonly id: number,
public readonly createdAt: Date,
public name: string
) {}
}
TypeScriptこの設計だと、
idとcreatedAtは「インスタンスが存在するなら必ずある」- かつ「途中で変わらない」
という強い前提が生まれます。
この前提があると、
他のメソッドの実装もシンプルになります。
class User {
// さっきと同じ
getDisplayLabel(): string {
// id / createdAt が「必ずある」「変わらない」前提で書ける
return `${this.id} (${this.createdAt.toISOString()}) ${this.name}`;
}
}
TypeScript「この値は、生成時に必ず決まっていて、その後も変わらない」
と思ったら、
コンストラクタ必須引数+readonly をセットで使う、
と覚えておくといいです。
readonlyとpublic / privateの組み合わせ
public readonly:外から“見えるけど変えられない”
class User {
constructor(
public readonly id: number,
public name: string
) {}
}
const u = new User(1, "Taro");
console.log(u.id); // 見える
// u.id = 2; // でも変えられない
TypeScriptpublic readonly は、
- 「外から見える」
- 「外からも中からも再代入できない(コンストラクタ以外)」
という組み合わせです。
「ID は外から参照されるけど、誰にも書き換えさせたくない」
みたいなときにピッタリです。
private readonly:中からも“定数扱い”
class Service {
private readonly baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
fetch(path: string) {
return fetch(this.baseUrl + path);
}
}
TypeScriptprivate readonly は、
- 外から見えない
- 中からも再代入できない
という、完全に「内部定数」的な扱いになります。
「このクラスの内部設定として固定したい値」は、private readonly にしておくと安心です。
まとめ:readonlyプロパティの活用を自分の言葉で整理すると
最後に、あなた自身の言葉でこう整理してみてください。
readonlyは「コンストラクタ以外で再代入できなくする」スイッチ- 「生成時に決まって、その後変わらない前提の値」に付けると強い
- ID・作成日時・設定値・性質フラグなどは積極的に readonly 候補
public readonlyは「外から見えるけど変えられない」private readonlyは「クラス内部の定数」- 配列やオブジェクトに付けたときは「プロパティの再代入は防げるが、中身の変更は別問題」と理解しておく
今書いているクラスを1つ開いて、
プロパティを上から眺めながら、
「これ、本当は途中で変わってほしくないよな?」
「これ、readonly にしたらロジックがスッキリしない?」
と一つずつ問い直してみてください。
readonly をちゃんと使い始めると、
クラスが「なんとなく動くもの」から、
“ルールが型に刻まれた、信頼できる設計図” に変わっていきます。
