【入門編】 Mapped Typesにおけるreadonly修飾子の付与と削除 – TypeScript実践ガイド

皆さん、こんにちは! TypeScriptの世界へようこそ!

私は長年、このTypeScriptという素晴らしい言語と深く向き合ってきましたが、その奥深さと、開発者の強い味方になってくれる懐の広さにはいつも驚かされます。特に、型システムを駆使してコードの安全性を高める仕組みは、一度知ってしまうと手放せなくなる魅力があります。

今日は、そんなTypeScriptの型システムの中でも、ちょっとだけ「おっ、これは便利そう!」と感じるであろう、そして実務でもよく顔を出す「Mapped Types」という強力な機能を使って、`readonly`修飾子を動的に付与したり、逆に剥がしたりする方法について、とことん優しく、そして現場の知見を交えながら解説していきますね。

「Mapped Types?」「readonly?」…なんだか難しそうな言葉が並んで、ちょっと身構えちゃいましたか? 大丈夫、安心してください。コーヒーでも飲みながら、リラックスして読み進めてくださいね。まるで、大切な書類を整理するような、身近な感覚で理解できるよう、丁寧に紐解いていきましょう。

—

🔒 `readonly`って、どんなガードマンさん?

まず「`readonly`」という言葉から、想像つくでしょうか? 「読み取り専用」という意味合いですね。
これはTypeScriptにおけるプロパティの修飾子の一つで、一度値が設定されたら、後からその値を変更できないようにする、という役割を持っています。

例えるなら、大切な資料が入ったファイルに「閲覧のみ可。改変禁止!」というスタンプを押すようなイメージです。一度スタンプを押したら、もうその中の書類を書き換えたり、破ったりはできません。もし無理に書き換えようとすると、TypeScriptが「コラコラ!それはダメだぞ!」と優しく、しかし確実に教えてくれます。

なぜ、そんなガードマンさんが必要なんでしょう?
それは、「意図しないデータの変更を防ぎ、コードの信頼性と安全性を高めるため」です。特にチーム開発や大規模なアプリケーションでは、どこかでデータが予期せぬ形で変更されてしまうと、思わぬバグに繋がることがよくあります。そんな時、`readonly`は強力な味方になってくれるんですよ。

まずは、簡単な例を見てみましょう。

type UserProfile = {
id: string;
name: string;
email: string;
};

// 通常のUserProfileは、プロパティを後から変更できます
let user: UserProfile = {
id: “u001”,
name: “山田 太郎”,
email: “taro@example.com”,
};

user.name = “山田 花子”; // OK!名前を変更できます
console.log(user); // { id: ‘u001’, name: ‘山田 花子’, email: ‘taro@example.com’ }

// では、idだけは絶対に変わってほしくない!という時
type ImmutableUserProfile = {
readonly id: string; // ここにreadonlyを付けました!
name: string;
email: string;
};

let immutableUser: ImmutableUserProfile = {
id: “u002”,
name: “田中 次郎”,
email: “jiro@example.com”,
};

// immutableUser.id = “u003”; // エラー! ‘id’ は読み取り専用プロパティなので変更できません。
immutableUser.name = “田中 一郎”; // これはOK!id以外のプロパティは変更できます。
console.log(immutableUser); // { id: ‘u002’, name: ‘田中 一郎’, email: ‘jiro@example.com’ }

このように、プロパティの前に手動で`readonly`を付けることで、そのプロパティが変更不可になります。シンプルで分かりやすいですよね!

🗺️ Mapped Typesで型を「魔法のように変換」する!

さて、本日の主役の一人、「Mapped Types」の登場です。
「マッピング(Mapping)」って聞くと、地図を広げて道順を確認するようなイメージが浮かびませんか? Mapped Typesもまさにそんな感じで、既存の型を「地図」のように広げて、その中身(プロパティ)を一つ一つ取り出し、新しい形に「変換」して新しい型を作り出す、ということができるんです。

例えるなら、ある商品の設計図(既存の型)があったとして、その設計図を元に、全ての部品に「防水加工」を施したり、逆に「防水加工を外して軽量化」したりするような、そんな柔軟な工場ラインをイメージしてみてください。

このMapped Typesを使うと、手動で一つ一つ`readonly`を付けたり外したりする手間から解放され、非常に効率的に型を操作できるようになります。

Mapped Typesの基本形(おまじない)

Mapped Typesは、このような形をしています。

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

ちょっと複雑に見えますか? 大丈夫、一つずつ分解すれば難しくありません。

  • `NewType`: `T`という既存の型を受け取って、`NewType`という新しい型を作るよ、という宣言です。
  • `[P in keyof T]`: ここが「魔法のループ」です!
  • `keyof T`: 型`T`が持つ全てのプロパティ名(キー)をリストアップします。例えば`UserProfile`なら`’id’ | ‘name’ | ‘email’`ですね。
  • `P in …`: そのリストアップされたプロパティ名を、一つずつ`P`という変数に入れて、ループを回します。
  • `T[P]`: ループで取り出されたプロパティ`P`が、元の型`T`ではどんな型だったか、という情報を指します。

この基本形だと、元の型と全く同じ型が生成されるだけなので、これだけではあまり意味がありません。しかし、このループの中でプロパティに手を加えることで、様々な型変換が可能になるのです!

🔑 Mapped Typesで`readonly`を「まとめて付与」する!

では、いよいよ本題です。先ほどの`UserProfile`の全てのプロパティに、一括で`readonly`を付けたい!という場合を考えましょう。
もしプロパティが100個あったら、手動で100回`readonly`と書くのは大変ですよね。そんな時にMapped Typesが輝きます。

先ほどの基本形に、たった一つ「`readonly`」というキーワードを追加するだけです。

type Readonly = {
readonly [P in keyof T]: T[P];
// ↑ここに `readonly` を追加するだけ!
};

どうでしょう?シンプルですよね!

この`Readonly`という型は、TypeScriptが標準で提供している`Readonly`ユーティリティ型と全く同じものです。つまり、皆さんは今、TypeScriptのコアなユーティリティ型を自作する方法を知ったことになります!すごい!

実際に使ってみましょう。

type Product = {
name: string;
price: number;
stock: number;
};

// Product型の全てのプロパティをreadonlyにした型を生成!
type ImmutableProduct = Readonly;

let myProduct: Product = {
name: “高級コーヒー豆”,
price: 1500,
stock: 100,
};

myProduct.stock = 99; // OK!通常のProductは変更可能

let immutableMyProduct: ImmutableProduct = {
name: “高級コーヒー豆”,
price: 1500,
stock: 100,
};

// immutableMyProduct.stock = 99; // エラー! ‘stock’ は読み取り専用プロパティなので変更できません。
// immutableMyProduct.price = 1600; // エラー! ‘price’ は読み取り専用プロパティなので変更できません。

console.log(immutableMyProduct); // { name: ‘高級コーヒー豆’, price: 1500, stock: 100 }

これで、`Product`型のどんなオブジェクトも、`ImmutableProduct`型として扱うことで、安全に「閲覧のみ」の状態にできます。例えば、APIから取得したデータのように、一度受け取ったらもう変更してはいけない情報などに使うと非常に便利ですよ。

🔓 Mapped Typesで`readonly`を「まとめて削除」する!

では、今度は逆のパターンです。
もともと`readonly`が付いている型を、一時的に変更可能な状態にしたい、というケースも出てくるかもしれません。例えば、設定ファイルは通常`readonly`で読み込みますが、編集画面では一時的に変更可能にしたい、といった場合ですね。

この時も、Mapped Typesが大活躍します。
今度は先ほどの`readonly`の前に、たった一つ「`-`(ハイフン)」を付けるだけです。

type Mutable = {
-readonly [P in keyof T]: T[P];
// ↑ここに `-readonly` を追加するだけ!
};

この`-readonly`という記述は、「`readonly`修飾子を削除する」という意味になります。まるで魔法が解ける呪文みたいですよね!

こちらも、TypeScriptが標準で提供している`Mutable`のようなユーティリティ型(厳密には`Partial`や`Required`などと組み合わせて使うことが多いですが、概念としては同じです)を自作していることになります。

実際に使ってみましょう。

type Config = {
readonly theme: string;
readonly language: “ja” | “en”;
readonly userId: string;
};

// Config型は全てreadonlyなので、そのままでは変更できません。
let appConfig: Config = {
theme: “dark”,
language: “ja”,
userId: “user_123”,
};

// appConfig.theme = “light”; // エラー! ‘theme’ は読み取り専用プロパティなので変更できません。

// Mutable型を使って、readonlyを解除した型を生成!
type WritableConfig = Mutable;

let editableConfig: WritableConfig = {
theme: “dark”,
language: “ja”,
userId: “user_123”,
};

// editableConfigは、readonlyが解除されているので変更可能!
editableConfig.theme = “light”; // OK!テーマを変更できます。
editableConfig.language = “en”; // OK!言語を変更できます。
// editableConfig.userId = “user_456”; // これは意味的に変更すべきではないが、型の上では変更可能になる

console.log(editableConfig);
// { theme: ‘light’, language: ‘en’, userId: ‘user_123’ }

これで、`readonly`な型を、一時的に変更可能な型に変換する術を身につけました。これは、ライブラリが提供する型が全て`readonly`になっていて、それを変更したい時に非常に役立ちます。

✨ ちょっとだけ深い話:`readonly`は「浅いガードマン」

ここで一つだけ、現場からのアドバイスをさせてください。
`readonly`修飾子は非常に便利ですが、一つだけ注意点があります。それは、`readonly`は「浅い(shallow)」変更不可であるということです。

どういうことかというと、オブジェクトの中にさらにオブジェクトや配列がネストされている場合、外側のオブジェクトには`readonly`が適用されても、内側のオブジェクトや配列のプロパティまでは保護してくれない、という意味です。

例えるなら、宝箱(外側のオブジェクト)に鍵をかけたとしても、その宝箱の中に入っている「別の小さな箱」(内側のオブジェクト)には、また別の鍵をかけないと開けられてしまう、というイメージですね。

type UserSettings = {
readonly id: string;
readonly preferences: {
theme: string;
notifications: boolean;
};
};

let settings: UserSettings = {
id: “setting_001”,
preferences: {
theme: “dark”,
notifications: true,
},
};

// settings.id = “setting_002”; // エラー! idはreadonlyなので変更できません。

// preferencesオブジェクト自体はreadonlyではないので、中身は変更可能!
settings.preferences.theme = “light”; // OK!変更できてしまう!
settings.preferences.notifications = false; // OK!変更できてしまう!

console.log(settings);
// { id: ‘setting_001’, preferences: { theme: ‘light’, notifications: false } }

このように、`preferences`オブジェクト自体は`readonly`ではないため、その中のプロパティは変更できてしまいます。もしネストされたオブジェクトの奥深くまで全て`readonly`にしたい場合は、別途「`DeepReadonly`」のような型を自分で定義するか、ライブラリを使う必要があります。

これは初学者の皆さんには少し先の知識かもしれませんが、`readonly`を使う上で意識の片隅に置いておくと、将来きっと役に立つはずです。

🚀 まとめと、これからの皆さんへ

今日は、TypeScriptの`readonly`修飾子と、それをMapped Typesという強力な仕組みを使って、動的に付与したり、削除したりする方法を見てきました。

  • `readonly`: プロパティを「読み取り専用」にするガードマンさん。意図しない変更を防ぎます。
  • Mapped Types: 既存の型を元に、プロパティを一つずつ変換して新しい型を生成する魔法の工場ライン。
  • `readonly`を付与: `type ReadonlyType = { readonly [P in keyof T]: T[P]; };`
  • `readonly`を削除: `type WritableType = { -readonly [P in keyof T]: T[P]; };`

初めて`readonly`やMapped Typesに触れた方は、「うわー、なんか難しそう…」と感じたかもしれません。でも、大丈夫です!これらの概念は、TypeScriptがコードの安全性を高め、長期的なメンテナンス性を向上させるための、非常に強力なツールなんです。

最初は、今回紹介したコードを実際にエディタに貼り付けて、自分で動かしてみることから始めてみてください。エラーが出たり、期待通りの動きにならなかったりするかもしれませんが、その「なぜ?」を解決していく過程で、皆さんのTypeScriptの理解はどんどん深まっていきます。

焦る必要は全くありません。一歩一歩、着実に理解を深めていけば、きっとTypeScriptの強力な恩恵を享受できるようになるでしょう。皆さんのこれからのTypeScript学習が、楽しく、そして実り多いものになるよう、心から応援しています!

また次の機会に、TypeScriptの深淵な世界でお会いしましょう!

コメント

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