こんにちは!TypeScriptの世界へようこそ。
日々たくさんのコードを書いていると、「このオブジェクトの値、うっかり書き換えてほしくないんだけどな…」という場面に出くわしませんか?
他の言語(JavaやC#など)を触ってきた方なら、「イミュータブル(不変)」という言葉はお馴染みかもしれませんね。TypeScriptでも、オブジェクトのプロパティを「読み取り専用」にする強力な仕組みが用意されています。
今回は、Interfaceにおける `readonly` 修飾子を取り上げます。ここをしっかりクリアすれば、TypeScriptの型安全性の基本はバッチリマスターできますよ。一緒に本質を深掘りしていきましょう!
—
1. なぜ `readonly` が必要なのか?(TypeScriptの型は「幻」である件)
まず、TypeScriptの大きな特徴を一つ思い出してください。
TypeScriptの型システムは、私たちがコードを書くとき(コンパイル時)にだけ存在する「幻」です。コードがJavaScriptに変換されて実行されるとき、型情報はすべてきれいさっぱり消え去ります。
ということは、普通のオブジェクトは、JavaScriptとして実行された瞬間に「誰でも書き換え放題」の状態になってしまいます。
interface User {
id: number;
name: string;
}
const user: User = { id: 1, name: ‘Alice’ };
// TypeScriptはコンパイル時にエラーを出さないが…
// 実行時には普通に書き換わってしまう!
// user.name = ‘Bob’; // 実際にはJavaScriptでは変更可能
「あれ? 型をつけても書き換えられちゃうの?」と不安になりますよね。
そこで登場するのが、Interfaceのプロパティにつける `readonly` 修飾子 です。これを使うことで、コンパイラが「おいおい、そこ書き換えようとしてるけどダメだよ!」と厳しく目を光らせてくれるようになります。
—
2. `readonly` の基本的な使い方とコードの動き
百聞は一見にしかず。実際のコードを見てみましょう。
// 1. Interfaceの定義でプロパティの前に `readonly` をつける
interface Article {
readonly id: string;
readonly title: string;
content: string; // こちらは readonly ではない
}
// オブジェクトの作成
const myArticle: Article = {
id: ‘art-001’,
title: ‘TypeScriptの魅力’,
content: ‘TypeScriptは素晴らしい言語です…’,
};
// — ここからがコンパイラによるチェック —
// 2. 読み取りは自由に行える
console.log(myArticle.title); // 正常に動作する:「TypeScriptの魅力」
// 3. 通常のプロパティは変更できる
myArticle.content = ‘内容をアップデートしました!’; // OK
// 4. readonlyプロパティを変更しようとすると…?
// myArticle.title = ‘新しいタイトル’;
// ❌ ここでTypeScriptのコンパイルエラーが発生します!
// > Cannot assign to ‘title’ because it is a read-only property. (TS2540)
このように、`readonly` がついたプロパティに対して再代入(値の書き換え)を行おうとすると、TypeScriptのコンパイラが即座にエラーを吐いて教えてくれます。これで、うっかりミスを未然に防ぐことができますね。
—
3. 初学者が陥りやすい罠:ディープコピーと「浅い(Shallow)イミュータビリティ」
ここで、少し踏み込んだ話をさせてください。
「`readonly` をつければ、オブジェクト全体が絶対に安全だ!」と思っていませんか? 実は、ここに多くの開発者がハマる落とし穴があります。
TypeScriptの `readonly` は、「そのプロパティが指す参照そのものの書き換え」を防ぐものであって、ネストした(入れ子になった)オブジェクトの内部まで自動的に保護してくれるわけではありません。
図解してみましょう。
[myConfig] (readonlyなしの変数)
│
├─ appName: “AwesomeApp” (プリミティブ型なので安全)
│
└─ database (readonly指定されている)
│
▼
{ host: “localhost”, port: 5432 } <-- ⚠️ この「中身」は書き換え可能!
実際のコードで確認してみましょう。
interface DatabaseConfig {
readonly host: string;
port: number;
}
interface AppConfig {
readonly appName: string;
readonly db: DatabaseConfig; // オブジェクトがネストしている
}
const config: AppConfig = {
appName: 'MyService',
db: {
host: 'localhost',
port: 3306,
},
};
// ❌ これはエラーになる(db自体の参照を別のオブジェクトに差し替えようとしたため)
// config.db = { host: 'remote', port: 5432 };
// ⚠️ しかし、dbの「中身のプロパティ」は書き換えられてしまう!
config.db.host = 'production-server.com'; // コンパイルエラーにならない!
「えっ、`readonly` をつけたのに書き換えられちゃうの!?」と驚かれたかもしれません。
そうなんです。Interfaceの `readonly` は「浅い(Shallow)不変性」を提供します。もしネストしたオブジェクトも含めて完全に不変にしたい場合は、TypeScriptが提供するユーティリティ型である `Readonly
(※ちなみに、TypeScript 4.5以降では `readonly` を再帰的に適用するテクニックなども使われますが、まずは「基本の `readonly` は表面(トップレベル)を守るもの」と覚えておけば間違いありません!)
—
4. 配列(Array)やタプルにおける `readonly` の世界
オブジェクトだけでなく、配列をイミュータブルに扱いたい場面も非常によくありますよね。TypeScriptでは、配列の型に対しても `readonly` を適用できます。
書き方にはいくつかバリエーションがありますが、実務でよく使われるのは以下の2つです。
// パターンA: readonly T[] 記法
interface UserGroup {
readonly memberNames: readonly string[];
}
const group: UserGroup = {
memberNames: [‘Alice’, ‘Bob’, ‘Charlie’],
};
// ❌ 配列の要素を書き換えることはできない
// group.memberNames[0] = ‘Dave’; // エラー!
// ❌ 要素を追加(push)したり削除(pop)したりするメソッドも存在しない扱いになる
// group.memberNames.push(‘Eve’); // エラー!
配列に `readonly` を付与すると、`push` や `pop`、直接の要素代入といった「配列を破壊する操作(Mutating methods)」の型定義が消え去ります。これにより、意図しないデータの書き換えてバグを生むリスクを綺麗に排除できるというわけです。
—
5. まとめ:実務で `readonly` をどう活用すべきか?
ここまで、Interfaceにおける `readonly` の基本から、少し深い挙動までを見てきました。最後に、実務で活かすためのガイドラインをまとめます。
1. 基本方針として「まずは `readonly` をつける」癖をつける
アプリケーションの状態や、関数の引数で受け取る設定オブジェクトなど、「一度作ったら変更されるべきではないデータ」には、積極的に `readonly` を付与しましょう。予期せぬバグの9割はこれで防げます。
2. 「変更されないこと」を型でドキュメント化する
`readonly` がついているコードは、他の開発者(あるいは未来の自分)に対して「このデータは安全に読み取っていいよ。ここで書き換えられる心配はないよ」という強力なメッセージ(ドキュメント)になります。
型システムを味方につければつけるほど、TypeScriptでの開発は心地よいものになっていきますよ。
ここをクリアできれば、あなたの書くコードの堅牢性は一段と跳ね上がります。ぜひ、今日のコードから `readonly` を意識して取り入れてみてくださいね!