【実務・中級編】 ReadonlyとMutableユーティリティ型 – TypeScript実践ガイド

「不変性」を制する者はバグを制す:ReadonlyとMutableの実践的活用術

こんにちは。現場でバリバリコードを書いている諸君、お疲れ様です。

今日はTypeScriptにおける「データの安全性」について語ろうと思う。プロジェクトが大きくなればなるほど、ある日突然どこかでデータが書き換えられ、原因不明のバグに頭を抱える……そんな経験、一度や二度はあるだろう?

特に、状態管理やAPIから返ってきたレスポンスを不用意に弄り回すと、Reactのレンダリングがおかしくなったり、意図しない副作用が連鎖したりする。これを防ぐための強力な武器が `Readonly` 型であり、逆に「今はどうしても書き換えが必要だ!」という時のための `Mutable` 型だ。

今日は、教科書的な説明はサクッと飛ばして、現場で明日から使える「プロの知見」を叩き込むぞ。

—

1. Readonly:なぜ「書き込み不可」が最強の防御なのか

TypeScriptの `Readonly` は、オブジェクトの全プロパティを読み取り専用に変換する。内部的には、各プロパティに `readonly` 修飾子が付与されるだけだが、この「コンパイル時のチェック」こそが開発者の命を救うんだ。

ブラウザの実行環境(V8エンジンなど)には、実は「Readonlyだから書き込ませない」という強力なバリケードはない。あくまでこれはTypeScriptのコンパイラが「お前、そこ書き換えるつもり? コードの意図と違うだろ」と開発者に警告するためのものだ。

しかし、この「型による規律」こそが、大規模開発におけるヒューマンエラーを劇的に減らす。

type User = {
id: number;
name: string;
};

// 読み取り専用に変換
const user: Readonly = { id: 1, name: “Alice” };

// 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 : T[P];
};

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` を検討せよ。

型定義は単なる記号遊びじゃない。チーム全員が同じ言語で「ここには触るな、ここは変えていい」という意思疎通を行うための「共通の地図」なんだ。

この感覚を身につければ、君の書くコードは一段上のレベルに達するはずだ。さあ、エディタを開いて、プロジェクトの型定義をもう一度見直してみようじゃないか。

コメント

タイトルとURLをコピーしました