おい、最近チームで「うっかりイミュータブル(変更不可)にすべきオブジェクトの値を書き換えてしまい、画面がバグった」なんて笑えない事故を起こしていないか?
中級に差し掛かったエンジニアなら、「変数の値はむやみに書き換えない(イミュータブル性)」がどれほど正義か、頭では分かっているはずだ。だがな、TypeScriptの型定義をサボっていると、コンパイラは平気な顔をして「お、ここ書き換えてるね、OKOK!」と通してしまう。そして本番環境でユーザーが涙を流す。
今日は、そんな悲劇を型レベルで完全封鎖するための最強の武器、`Readonly
—
1. `Readonly` の基本と、ブラウザの裏側で起きていること
まずは基本のおさらいだ。`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
// ❌ コンパイルエラー:配列の要素を追加・変更することは許されない
// allowedRoles.push(‘super-admin’);
// allowedRoles[0] = ‘guest’;
// 変更不可のタプル型
const coordinate: Readonly<[number, number]> = [35.6812, 139.7671]; // 東京駅の緯度経度
// ❌ コンパイルエラー
// coordinate[0] = 0;
どうだ? こうやって型をガチガチに固めておくと、チームメンバーが勝手に配列を `push()` して予期せぬバグを生むリスクを、見事にゼロにできる。コードレビューで「ここ、勝手に書き換えちゃダメですよ」なんて不毛な指摘をする必要すらなくなるんだ。
—
3. 注意点:浅いコピー(Shallow Readonly)の罠
ここで、中級エンジニアなら絶対に知っておかなければならない「ダークサイド」について話しておこう。
実は、標準の `Readonly
ネストした(入れ子になった)オブジェクトの中身までは守ってくれない。
以下のコードを見てくれ。
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
: T extends Map
? ReadonlyMap
: 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
君の書くコードの安全性と信頼性が、一段階も二段階も跳ね上がるはずだ。それじゃ、次の現場でもスマートなコードを頼むぜ!

コメント