「不変性」を制する者はバグを制す:ReadonlyとMutableの実践的活用術
こんにちは。現場でバリバリコードを書いている諸君、お疲れ様です。
今日はTypeScriptにおける「データの安全性」について語ろうと思う。プロジェクトが大きくなればなるほど、ある日突然どこかでデータが書き換えられ、原因不明のバグに頭を抱える……そんな経験、一度や二度はあるだろう?
特に、状態管理やAPIから返ってきたレスポンスを不用意に弄り回すと、Reactのレンダリングがおかしくなったり、意図しない副作用が連鎖したりする。これを防ぐための強力な武器が `Readonly` 型であり、逆に「今はどうしても書き換えが必要だ!」という時のための `Mutable` 型だ。
今日は、教科書的な説明はサクッと飛ばして、現場で明日から使える「プロの知見」を叩き込むぞ。
—
1. Readonly:なぜ「書き込み不可」が最強の防御なのか
TypeScriptの `Readonly
ブラウザの実行環境(V8エンジンなど)には、実は「Readonlyだから書き込ませない」という強力なバリケードはない。あくまでこれはTypeScriptのコンパイラが「お前、そこ書き換えるつもり? コードの意図と違うだろ」と開発者に警告するためのものだ。
しかし、この「型による規律」こそが、大規模開発におけるヒューマンエラーを劇的に減らす。
type User = {
id: number;
name: string;
};
// 読み取り専用に変換
const user: Readonly
// user.name = “Bob”; // コンパイルエラー!ここで止めるのがTypeScriptの真骨頂
console.log(user.name); // 読み取りはOK
現場のTips: APIのレスポンス型は、基本的にすべて `Readonly` で扱うべきだ。「取得したデータはサーバーの真実であり、クライアントが勝手に改変してはいけない」というアーキテクチャ上の合意を型で強制する。これが堅牢なフロントエンドの第一歩だ。
—
2. Mutable:あえて「牙」を剥くための逆転の発想
一方で、ライブラリとの兼ね合いや、どうしても既存のオブジェクトを一時的に書き換えたいというケースもある。標準のTypeScriptには `Mutable` というユーティリティ型は存在しない。だから、自分で定義する必要がある。
これは、`readonly` 修飾子を「剥ぎ取る」という操作だ。
// 魔法のようなMutable型。Readonlyの逆を行う
type Mutable
-readonly [P in keyof T]: T[P];
};
interface Config {
readonly theme: string;
readonly port: number;
}
const myConfig: Config = { theme: “dark”, port: 8080 };
// 型キャストして書き換える(奥の手だ、乱用は厳禁)
const mutableConfig = myConfig as Mutable
mutableConfig.theme = “light”;
console.log(myConfig.theme); // “light” に変わってしまう
このコードを見るとわかる通り、`Mutable` は強力だが、同時に「不変性」を破壊する行為でもある。使うときは必ず「なぜここだけ書き換える必要があるのか」をコメントに残すか、副作用が局所的であることを証明できる状況で使うこと。
—
3. 実践:深いネストへの対処(DeepReadonly)
`Readonly` の弱点は、ネストされたオブジェクトまで再帰的に保護してくれないことだ。中級者なら、ここを突破する「DeepReadonly」を実装できるようになっておきたい。
// 再帰的にReadonlyを適用する型
type DeepReadonly
readonly [P in keyof T]: T[P] extends object ? DeepReadonly
};
type AppState = {
settings: {
theme: string;
};
};
const state: DeepReadonly
settings: { theme: “dark” }
};
// state.settings.theme = “light”; // 深い階層もしっかりコンパイルエラーになる
これを使えば、ReduxやZustandなどの状態管理ライブラリにおいて、不用意なstateの書き換えをコンパイルレベルで完全に封殺できる。
—
最後に:シニアからのアドバイス
「不変性(Immutability)」を保つことは、コードを「予測可能」にすることだ。予測可能なコードはテストが書きやすく、バグが入り込む余地が少ない。
1. 基本はReadonly: すべてのデータはデフォルトで読み取り専用と見なせ。
2. Mutableは「外科手術」: どうしても書き換えが必要な時だけ、限定的に使え。
3. 深い階層に注意: 入れ子構造になったデータには `DeepReadonly` を検討せよ。
型定義は単なる記号遊びじゃない。チーム全員が同じ言語で「ここには触るな、ここは変えていい」という意思疎通を行うための「共通の地図」なんだ。
この感覚を身につければ、君の書くコードは一段上のレベルに達するはずだ。さあ、エディタを開いて、プロジェクトの型定義をもう一度見直してみようじゃないか。

コメント