やあ、調子はどうだい?
最近、コードレビューをしていて「あ、ここ、うっかり書き換えられちゃってるよ……!」ってバグを踏むことが多くて頭を悩ませてないかい?特にReduxのステートや、APIから取ってきた初期設定オブジェクトなんかね。
フロントエンドの規模が大きくなればなるほど、「どこで誰がこのデータを書き換えたんだ!?」っていう不毛な犯人探しに時間を溶かすことになる。この地獄から抜け出すための特効薬が、今回解説する `Readonly
今回は、中級からもう一段階上の「型を制するシニア」へステップアップするための、実戦的な `Readonly
—
1. `Readonly` とは何か?(基本のおさらい)
TypeScriptの組み込み型(Utility Types)の一つである `Readonly
まずは、百聞は一見に如かず。コードを見てみよう。
// ユーザー情報を表す型
interface User {
id: number;
name: string;
}
// 通常のオブジェクト
const mutableUser: User = {
id: 1,
name: “Taro”,
};
// 後から書き換えが可能(お馴染みの状態)
mutableUser.name = “Jiro”; // OK!
// Readonly
const readonlyUser: Readonly
id: 1,
name: “Taro”,
};
// ここでコンパイルエラーが発生する!
// ❌ Cannot assign to ‘name’ because it is a read-only property.
readonlyUser.name = “Jiro”;
めちゃくちゃシンプルだろ?
「これの何が凄いの? 最初から `interface` のプロパティに `readonly` つければいいじゃん」って思ったそこの君、鋭いね。
でも、考えてみてほしい。他人が作ったライブラリの型や、APIのレスポンス型、あるいは「ある文脈では書き換え可能にしたいけど、別の関数には絶対に読み取り専用として渡したい」というケースにおいて、元の型を汚さずにその場でサクッとイミュータブルを強制できるのが `Readonly
—
2. ブラウザの裏側で何が起きているのか?(TypeScriptの幻想とJSの現実)
ここでシニアとして君に絶対に知っておいてほしい「残酷な真実」がある。
TypeScriptの `Readonly
ブラウザのJavaScriptエンジン(V8など)が実行しているのは、以下のような素のJavaScriptコードだ。
// TypeScriptの Readonly
const readonlyUser = {
id: 1,
name: “Taro”,
};
// JSのランタイム上では、普通に書き換えが可能!
readonlyUser.name = “Jiro”; // エラーは起きない
「えっ、じゃあ意味ないじゃん!」って焦ったかい?
安心してほしい。TypeScriptのコンパイラが「お前、そこ書き換えようとしてるけど型違反だぞ!」ってビルド時(あるいはエディタ上)に強力にガードしてくれるから、バグを未然に防げるんだ。
ただし、「完全に実行時でもオブジェクトを凍結したい(イミュータブルを保証したい)」という場合は、TypeScriptの型システムだけでは不十分な点に注意してくれ。その場合は、JavaScriptの標準機能である `Object.freeze()` と組み合わせるのがプロの現場の作法だ。
interface Config {
readonly endpoint: string;
readonly timeout: number;
}
// 型で縛りつつ、ランタイムでも書き換えを完全封鎖する
const appConfig: Readonly
env: “production”,
timeout: 5000,
});
// 万が一、JSとして実行時に書き換えようとしても、
// strictモードであれば TypeError がスローされる
// appConfig.timeout = 10000;
—
3. 現場で役立つ!コピペで使える実践的サンプルコード
さて、ここからが本番だ。実務でよく遭遇する「ネストしたオブジェクトの罠」と、その解決策を見ていこう。
罠:`Readonly` は「浅い(Shallow)」という事実
実は、標準の `Readonly
以下のコードを見てくれ。
interface Team {
name: string;
leader: {
name: string;
};
}
const myTeam: Readonly
name: “Frontend Guild”,
leader: {
name: “Taro”,
},
};
// 外側はちゃんとエラーになる
// myTeam.name = “Backend Guild”; // ❌ エラー!
// 【注意】内側のオブジェクトは書き換えられてしまう!
myTeam.leader.name = “Jiro”; // ⭕️ あれっ、通っちゃったよ……!
これに気づかずにハマる中級エンジニアが本当に多い。再帰的にすべてをイミュータブルにしたい場合は、自作のユーティリティ型(DeepReadonly)を作る必要がある。
解決策:再帰的にすべてを凍結する `DeepReadonly`
実務のボイラープレートとして、共通の型定義ファイル(`types/common.d.ts` など)にこれを仕込んでおくと、チームメンバーから神扱いされること間違いなしだ。
// 再帰的にすべてのプロパティを Readonly にするユーティリティ型
export type DeepReadonly
? T
: T extends Map
? ReadonlyMap
: T extends Set
? ReadonlySet
: T extends object
? { readonly [K in keyof T]: DeepReadonly
: T;
// — 使用例 —
interface ComplexState {
user: {
profile: {
name: string;
settings: {
darkMode: boolean;
};
};
};
}
const state: DeepReadonly
user: {
profile: {
name: “Alice”,
settings: {
darkMode: true,
},
},
},
};
// すべての階層で書き換えがコンパイルエラーになる!
// state.user.profile.settings.darkMode = false;
// ❌ Cannot assign to ‘darkMode’ because it is a read-only property.
この `DeepReadonly
—
4. シニアからのアドバイス:いつ使うべきか?
なんでもかんでも `Readonly
俺がチームに推奨しているベストプラクティスは以下の通りだ。
1. 関数コンポーネントの Props
Propsは基本的に子コンポーネント側で勝手に書き換えるべきではない。そのため、`React.FC
2. 状態管理(Redux, Zustand, React Contextなど)のステート型
グローバルステートや、コンポーネントのイミュータブルな状態定義には迷わず使うべき。予期せぬミューテーションによる「再レンダリングが走らないバグ」を根絶できる。
3. APIクライアントのレスポンス型
バックエンドから受け取ったデータは「変更不能な事実(Facts)」であるべきだ。フロントエンド側で加工が必要な場合は、新しいオブジェクトをスプレッド構文などでクローンして作るべきであり、元のレスポンスを直接いじらない担保として `Readonly` が最高の防壁になる。
—
まとめ
TypeScriptの `Readonly
「このデータ構造のこの部分は、誰も勝手に書き換えてはいけない」という開発チーム全体の意思表示であり、堅牢なアーキテクチャを支えるための強力な契約書なんだ。
最初は少し面倒くさく感じるかもしれないけれど、一度この安全性に慣れると、普通の mutable なコードを書くのが怖くなるはずだ。
さあ、今日のプロダクトのコードを開いて、無防備に晒されているステートやPropsに `Readonly` をブチ込んでやろうぜ。何か質問があったら、いつでも俺のところへ聞きに来てくれ。ハッピー・コーディング!

コメント