【実務・中級編】 Readonly – TypeScript実践ガイド

おい、最近チームで「うっかりイミュータブル(変更不可)にすべきオブジェクトの値を書き換えてしまい、画面がバグった」なんて笑えない事故を起こしていないか?

中級に差し掛かったエンジニアなら、「変数の値はむやみに書き換えない(イミュータブル性)」がどれほど正義か、頭では分かっているはずだ。だがな、TypeScriptの型定義をサボっていると、コンパイラは平気な顔をして「お、ここ書き換えてるね、OKOK!」と通してしまう。そして本番環境でユーザーが涙を流す。

今日は、そんな悲劇を型レベルで完全封鎖するための最強の武器、`Readonly` について、実務の現場でどう使い倒すべきか、俺が徹底的に叩き込んでやる。ついてこい。

—

1. `Readonly` の基本と、ブラウザの裏側で起きていること

まずは基本のおさらいだ。`Readonly` は、TypeScriptが標準で用意しているユーティリティ型の一つで、渡された型 `T` のすべてのプロパティに `readonly` 修飾子を付与してくれる。

百聞は一見に如かず。まずはコードを見てくれ。

// ユーザー情報を表す型
interface User {
id: number;
name: string;
}

// 普通のオブジェクト
const mutableUser: User = {
id: 1,
name: ‘Taro Yamada’,
};

// 後から書き換えが可能(おっと、危険な香りがするぜ)
mutableUser.name = ‘Jiro Yamada’;

// さて、Readonly を使ってみよう
const safeUser: Readonly = {
id: 1,
name: ‘Taro Yamada’,
};

// ここでコンパイルエラー!
// Cannot assign to ‘name’ because it is a read-only property.(2540)
// safeUser.name = ‘Jiro Yamada’;

おっと、TypeScriptのコンパイラが「お前、それ読み取り専用だぞ!」と怒ってくれたな。これが `Readonly` の仕事だ。

ブラウザ(JavaScriptエンジン)の裏側では何が起きているのか?

ここで一つ、シニアとして君に大切な現実を伝えておかなければならない。
「TypeScriptの `Readonly` は、コンパイル時(開発時)にしか存在しない幻影だ」 ということだ。

JavaScriptのランタイム(V8エンジンなど)には、`readonly` という概念はネイティブには存在しない(`Object.freeze()` とは別物だ)。TypeScriptがトランスパイル(TypeScriptからJavaScriptへの変換)を行うと、`readonly` の記述は綺麗さっぱり消し飛ぶ。

つまり、ブラウザが実行しているJavaScriptのコード上では、普通にプロパティの書き換えができてしまう。
「じゃあ意味ないじゃん!」と思ったか? いや、大間違いだ。

コンパイル時にエラーを出してくれることで、「開発者がうっかりバグを混入させる未来」をIDE(VSCodeなど)の補完と赤波線で未然に防ぐ。これがTypeScriptの、そして `Readonly` の真価なのだ。

—

2. 現場で使える!実践的なサンプルコード

さて、基本が分かったところで、実務の現場でどうやってこれを活かすかだ。
APIから取得した設定情報や、Redux / Zustand などの状態管理で「絶対にイミュータブルに保ちたいデータ」を扱うシーンを想像してくれ。

以下のコードは、そのままコピペして明日からでもプロジェクトに組み込める実用的なパターンだ。

/

  • アプリケーションの設定を表す型

/
interface AppConfig {
readonly endpoint: string; // ここにも個別にreadonlyを貼れるが…
timeout: number;
retries: number;
}

/

  • APIから返ってきた設定を、絶対に改変不可の「神のデータ」として扱う関数
  • @param rawData サーバーからの生データ

/
function initializeConfig(rawData: AppConfig): Readonly {
// 開発者に「この設定オブジェクトは二度と変更するなよ」と型で強く意思表示する
return {
…rawData,
};
}

const config = initializeConfig({
endpoint: ‘https://api.example.com’,
timeout: 5000,
retries: 3,
});

// ❌ コンパイルエラー:設定のタイムアウトを勝手に書き換えようとする暴挙を防ぐ
// config.timeout = 10000;

// — 実務Tips: 配列やタプルにも Readonly は効く —

// 変更不可の文字列配列(ReadonlyArray と同義)
const allowedRoles: Readonly = [‘admin’, ‘editor’, ‘viewer’];

// ❌ コンパイルエラー:配列の要素を追加・変更することは許されない
// allowedRoles.push(‘super-admin’);
// allowedRoles[0] = ‘guest’;

// 変更不可のタプル型
const coordinate: Readonly<[number, number]> = [35.6812, 139.7671]; // 東京駅の緯度経度
// ❌ コンパイルエラー
// coordinate[0] = 0;

どうだ? こうやって型をガチガチに固めておくと、チームメンバーが勝手に配列を `push()` して予期せぬバグを生むリスクを、見事にゼロにできる。コードレビューで「ここ、勝手に書き換えちゃダメですよ」なんて不毛な指摘をする必要すらなくなるんだ。

—

3. 注意点:浅いコピー(Shallow Readonly)の罠

ここで、中級エンジニアなら絶対に知っておかなければならない「ダークサイド」について話しておこう。

実は、標準の `Readonly` は 「浅い(Shallow)」 適用範囲しか持たない。つまり、オブジェクトの「一段階目のプロパティ」しか読み取り専用にしてくれないのだ。
ネストした(入れ子になった)オブジェクトの中身までは守ってくれない。

以下のコードを見てくれ。

interface UserProfile {
name: string;
address: {
city: string;
zipCode: string;
};
}

const userProfile: Readonly = {
name: ‘Tanaka’,
address: {
city: ‘Tokyo’,
zipCode: ‘100-0001’,
},
};

// 1段階目は当然エラーになる(OK)
// userProfile.name = ‘Suzuki’;

// ⚠️ 注意:ネストしたプロパティは普通に書き換えられてしまう!
// TypeScriptはこれを見逃してしまうんだ。マジで危険だろ?
userProfile.address.city = ‘Osaka’;

おっと冷や汗をかいたか? そう、`Readonly` だけでは、ネストしたオブジェクトの改変までは防げない。

じゃあどうするか?(シニアの処方箋)

もし君のプロジェクトで、完全にネストの奥底までイミュータブルにしたい(Deep Readonlyにしたい)場合は、自前で再帰的なユーティリティ型を定義するか、実績のあるサードパーティライブラリ(`type-fest` の `ReadonlyDeep` など)を使うのが実務の定石だ。

自前で書くなら、こんな感じの型を共通定義ファイルに仕込んでおくとチームから絶賛されるはずだ。

/

  • ネストしたオブジェクトも含めて、すべての階層を強制的に読み取り専用にする(Deep Readonly)

/
type DeepReadonly = T extends Function | primitive
? T
: T extends Map
? ReadonlyMap, DeepReadonly>
: T extends Set
? ReadonlySet>
: T extends object
? { readonly [K in keyof T]: DeepReadonly }
: T;

// これを使えば、ネストした address も完全に鉄壁の守りになる!
const strictProfile: DeepReadonly = {
name: ‘Tanaka’,
address: {
city: ‘Tokyo’,
zipCode: ‘100-0001’,
},
};

// ❌ 完璧にコンパイルエラーになる!
// strictProfile.address.city = ‘Osaka’;

—

まとめ

`Readonly` は、単なる「型を縛るための面倒くさいルール」じゃない。
「このデータは安全ですよ」という意思表示であり、将来の自分やチームメンバーへの強力なメッセージ、そしてバグを未然に防ぐ最高の防壁だ。

実務でコードを書くときは、「このデータ、後から書き換える必要あるか?」と常に疑い、必要がないなら積極的に `Readonly`(あるいは `DeepReadonly`)を貼る癖をつけてほしい。

君の書くコードの安全性と信頼性が、一段階も二段階も跳ね上がるはずだ。それじゃ、次の現場でもスマートなコードを頼むぜ!

コメント

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