【入門編】Interfaceの「readonly」修飾子とImmutabilityの強制 – TypeScript コア・型システムの基礎解析バイブル

やあ。TypeScriptの深淵へようこそ。
日々コードを書いていると、「このデータ、どこかで書き換えられたくないな」と思う瞬間があるよね。そんな時、TypeScriptの`readonly`は強力な武器になる。

今日は、インターフェースにおける`readonly`と、ユーティリティ型である`Readonly`の違い、そしてその背後にある「型システムとしての挙動」について、現場の知見を交えて紐解いていこう。

—

1. `readonly`修飾子:コンパイラによる「静的なガード」

まずは基本から。インターフェース定義の中でプロパティに`readonly`を付けると、そのプロパティは「初期化時以外は代入不可」という制約を受けることになる。

interface User {
readonly id: number;
name: string;
}

const user: User = { id: 1, name: “Alice” };

user.name = “Bob”; // OK: nameはreadonlyではない
user.id = 2; // Error: Cannot assign to ‘id’ because it is a read-only property.

なぜこれが必要なのか?

TypeScriptの型システムは、「型が合っていればそれでいい」という構造的部分型(Structural Subtyping)を採用している。しかし、プログラムのバグの多くは「意図しないデータの書き換え」から生まれる。`readonly`を明示することで、コンパイラという強力な監視役を味方につけ、「ここを触ったら事故になる」という境界線を引くことができるんだ。

—

2. インターフェースの`readonly` vs `Readonly`

ここで多くの人が迷うのが、「インターフェースで個別に書くのと、`Readonly`で一括変換するのは何が違うのか?」という点だ。

Readonly の正体

`Readonly`は、TypeScriptが標準で用意している「Mapped Types(マップ型)」を使ったユーティリティ型だ。内部的にはこのような定義になっている。

type Readonly = {
readonly [P in keyof T]: T[P];
};

つまり、「Tの全てのプロパティをループして、強制的に`readonly`を付与する」という魔法をかけているんだ。

使い分けの基準

  • インターフェースの`readonly`: 「特定のプロパティだけは変更不可にしたい」というドメインモデルとしての設計上の制約を表現するのに向いている。
  • `Readonly`: 「このオブジェクトをこの関数内では絶対に触らせない(イミュータブルとして扱いたい)」という関数の契約(コントラクト)として使うのがスマートだ。

—

3. 陥りやすい「浅いコピー(Shallow)」の罠

ここからが本題だ。初学者が一番ハマりやすいのが、`readonly`は「浅い(Shallow)」という性質だよ。

interface Config {
readonly settings: {
theme: string;
};
}

const config: Config = { settings: { theme: “dark” } };

// ここに注目!
config.settings.theme = “light”; // なんと、エラーにならない!

なぜエラーにならないのか?
`readonly`が保証しているのは、`config.settings`というプロパティに「別のオブジェクトを再代入すること」だけだ。オブジェクトの中身(プロパティ)が書き換わることまでは防げないんだよ。

深い不変性(Deep Immutability)が必要な時は?

もし、構造全体をガチガチに守りたいなら、TypeScript 4.3以降であれば`readonly`の再帰的な適応を検討するか、あるいは`as const`(Const Assertion)を使いこなす必要がある。

// as const を使ったアプローチ
const config = {
settings: { theme: “dark” }
} as const;

config.settings.theme = “light”; // Error! 完全に読み取り専用になる

—

先輩からのアドバイス:どう使い分けるか

コードの「堅牢性」を高めるための考え方を伝授しておくね。

1. 「状態(State)」を持つオブジェクトには、意図的に`readonly`を刻め。 特に、APIレスポンスの型定義など、自分たちが制御できない外部からのデータは基本的に`readonly`で扱うと、不意の副作用を防げるよ。
2. 関数の引数には`Readonly`を検討せよ。 「この関数はデータを破壊しませんよ」という意思表示は、読み手に対する最高のドキュメントになる。
3. 深い階層は`as const`で守れ。 複雑な設定値や定数データは、再帰的に読み取り専用にするのが現代的なTypeScriptの作法だ。

—

まとめ

`readonly`は、単なる文法じゃない。「このデータはここから先、変化しない」というあなたの意志をコンパイラに伝えるための型レベルの宣言なんだ。

最初は少し窮屈に感じるかもしれない。けれど、この制約を使いこなせるようになると、実行時に発生する「なぜか値が変わっている」という不可解なバグが劇的に減るはずだよ。

ここをクリアすれば、君のTypeScriptレベルは確実に一段階上がる。焦らず、まずは手元のコードで`readonly`を付与してみることから始めてみよう。応援しているよ!

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