こんにちは!TypeScriptの世界へようこそ。
日々フロントエンドやバックエンドの開発でTypeScriptを書いていると、「このデータ、うっかり書き換えてほしくないんだけど、どうやって型で縛るのが正解なんだろう?」と悩む瞬間はありませんよね。特に、ReduxやZustandといった状態管理ライブラリを触るようになると、「不変性(イミュータビリティ)」の確保は避けて通れないテーマになります。
今回は、インターフェース(`interface`)のプロパティにつける `readonly` 修飾子と、型エイリアス(`type`)でよく使うユーティリティ型 `Readonly
ここをクリアすれば、TypeScriptの型システムに対する解像度がグッと上がりますよ。一緒にバッチリマスターしていきましょう!
—
1. まず基本のおさらい:不変性(Immutability)ってなに?
プログラムにおける「不変性」とは、「一度作ったオブジェクトや変数のデータを、後から変更(破壊)できないようにすること」です。
例えば、JavaScriptの通常のオブジェクトは、変数名が `const` で宣言されていても、中身のプロパティは自由に変更できてしまいます。
const user = { name: ‘Alice’ };
user.name = ‘Bob’; // エラーにならない!中身が書き換わってしまう(可変)
大規模なアプリケーションになってくると、「知らぬ間にどこかの関数でデータが書き換えられていて、バグの原因が特定できない……」という悪夢のような現象が起きます。これをコンパイルの時点でバシッと防いでくれるのが、TypeScriptの不変性機能なんです。
—
2. インターフェースの `readonly` 修飾子
まずは `interface` を使う方法です。インターフェースのプロパティ定義の頭に `readonly` をつけることで、そのプロパティへの「再代入」をコンパイルエラーとして検知させることができます。
interface User {
readonly id: number; // 読み取り専用!
name: string; // 書き換え可能
}
const user: User = { id: 1, name: ‘Alice’ };
// これはOK(nameはreadonlyではないため)
user.name = ‘Bob’;
// 🛑 ここでコンパイルエラー!
// 「Cannot assign to ‘id’ because it is a read-only property.」
user.id = 2;
💡 コンパイル時の裏側のハナシ
TypeScriptの `readonly` は、実はJavaScriptにトランスパイル(変換)された後には消滅します。実行時のJavaScript上では普通のオブジェクトとして動きますが、TypeScriptのコンパイラが「おっと、そこは書き換えちゃダメだよ」とビルド前に検知して止めてくれる仕組みです。そのため、ランタイム(実行時)のパフォーマンスを一切落とさずに型の安全性を高められます。
—
3. 型エイリアスの `Readonly` ユーティリティ型
次に、`type`(型エイリアス)や既存の型をまとめて保護したいときに便利な `Readonly
TypeScriptには、標準で用意されている便利な型(ユーティリティ型)がたくさんあります。その一つである `Readonly
type UserType = {
id: number;
name: string;
};
// Readonly
type ReadonlyUser = Readonly
const user: ReadonlyUser = { id: 1, name: ‘Alice’ };
// 🛑 idもnameも両方ともコンパイルエラーになる!
// user.id = 2;
// user.name = ‘Bob’;
「わざわざ一つずつ `readonly` を書くのは面倒だな」という時や、外部からインポートした型をまるっと不変に扱いたい時にめちゃくちゃ重宝します。
—
4. どっちを使うべき? 2つのアプローチの比較
「じゃあ、結局どっちを使えばいいの?」という疑問が湧いてきますよね。それぞれの特徴を整理してみましょう。
| 特徴 | `interface` の `readonly` | `type` + `Readonly
| :— | :— | :— |
| 主な用途 | オブジェクトの特定のプロパティだけを保護したいとき | 型全体のすべてのプロパティを一括で保護したいとき |
| 柔軟性 | プロパティ単位で「書けるもの」と「書けないもの」を混在させやすい | まるっと不変にすることで、データの純粋性を保ちやすい |
| 拡張性 | 宣言的マージ(Declaration Merging)が使える | ユーティリティ型やユニオン型と組み合わせやすい |
🛠️ 状態管理ライブラリ(Redux / Zustand など)でのベストプラクティス
現代のモダンなフロントエンド開発において、状態(State)は「完全にイミュータブル(変更不可)であるべき」という設計思想が主流です。
もしあなたが状態管理のストアの型を定義するなら、次のように `Readonly
// アプリケーションの全体の状態を表す型
interface AppState {
user: {
id: number;
name: string;
};
isAuthenticated: boolean;
}
// ストアのリーダー用(読み取り専用)の型として公開する
export type ReadonlyStoreState = Readonly
// 実際のコンポーネントや関数では、不変な状態として受け取る
function renderProfile(state: ReadonlyStoreState) {
// state.isAuthenticated = true;
// 🛑 コンパイルエラーになるため、意図しない書き換えを完全に防止できる!
console.log(`Welcome, ${state.user.name}`);
}
ここで注意深い読者ならお気づきかもしれません。実は、通常の `Readonly
基本の第一歩としては、まずトップレベルのプロパティを `Readonly
—
まとめ
今回は、インターフェースの `readonly` 修飾子と、型エイリアスの `Readonly
- ピンポイントで一部だけ保護したいなら、`interface` のプロパティに `readonly` をつける。
- オブジェクト全体を丸ごとイミュータブルとして扱いたいなら、`type` と `Readonly
` を組み合わせる。
この2つを適切に使い分けられるようになると、あなたの書くコードの安全性と信頼性は段違いに跳ね上がります。「コンパイラが味方をしてくれる」というTypeScriptの最大の強みを、ぜひ日々の開発で実感してみてくださいね。
それでは、また次回の記事でお会いしましょう!バッチリマスターしていきましょう!