【実務・中級編】 Readonly修飾子付きマップ型 – TypeScript実践ガイド

お疲れ。最近、君の書くコードを見ていて「おっ、いい感じに型安全の恩恵を受け始めているな」と感心していたところだ。でもな、中級からもう一段階上のシニアへステップアップする時に、みんなここで一回つまずくポイントがあるんだよ。それが今回話す「Readonly修飾子付きマップ型」だ。

「オブジェクトのプロパティをイミュータブル(変更不可)にしたい!」と思ったとき、とりあえず頭に `readonly` をつけるか、標準の `Readonly` を使うよな。でも、既存の型の特定のプロパティだけをイミュータブルにしたり、逆にサードパーティ製ライブラリのガチガチに固められた `readonly` を剥がして書き換え可能にしたりする必要に迫られたことはないか?

今回は、TypeScriptのマップ型(Mapped Types)における `readonly` の付与と削除(いわゆるモーダフィケーション)について、実務の現場で即座に使える実践的なノウハウを伝授しよう。

—

1. そもそもマップ型と `readonly` の関係ってどうなってるの?

TypeScriptのマップ型は、既存の型のプロパティを走査して、新しい型を動的に作り出すための強力な武器だ。この時、プロパティ名の前に対象の修飾子をつけることで、そのイミュータビリティを自在にコントロールできる。

ここで使える記号は主にこの2つだ:

  • `+readonly` (または単に `readonly`):プロパティを読み取り専用にする。
  • `-readonly`:プロパティの読み取り専用(`readonly`)を剥ぎ取る。

「え、 `-` なんて付けることあるの?」と思ったそこの君。あるんだよこれが。APIから返ってきたガチガチの型を、フォームの状態管理(State)用に一時的に書き換え可能にしたい時なんかには、この `-readonly` が無いと発狂することになる。

ブラウザの裏側でのTypeScriptの振る舞い

ちょっと待て、「ブラウザが裏側でどう処理しているか」気にならないか?
大前提として、TypeScriptの型システムはコンパイル時(トランスパイル時)の幻影だ。ブラウザのJavaScriptエンジン(V8など)が実行する段階では、型も `readonly` も綺麗さっぱり消え去っている。

だが、TypeScriptのコンパイラ(tsc)はこの `readonly` を厳しく監視している。
コンパイル時、`readonly` が付いたプロパティに対して再代入(`obj.prop = value`)を行おうとすると、AST(抽象構文木)のチェック段階で「おい、そこ書き換え不可だぞ」とTypeScriptのLSP(Language Service Protocol)が赤線を引いて教えてくれる。つまり、この機能の本質は、「不注意なバグをランタイムに行く前にコンパイラにボコボコに指摘してもらうための保険」なのだ。

—

2. 現場で即コピペして使える実践コード

百聞は一見にしかずだ。実際のフロントエンド開発でよく遭遇するユースケースをベースにしたコードを見ていこう。エディタに貼り付けて挙動を確認してみてくれ。

/

  • ユーザー情報のベース型
  • バックエンドから取得するイミュータブルなデータを想定

/
type User = {
readonly id: string;
readonly name: string;
readonly email: string;
};

/

  • 実践Tips 1: すべてのプロパティから readonly を剥ぎ取る型 (Mutable)
  • フォームの入力値や、一時的なDraft状態を管理する時に神のように活躍する

/
type Mutable = {
-readonly [K in keyof T]: T[K];
};

// 【解説】
// -readonly を使うことで、Userの各プロパティについていた readonly が綺麗に消え去る。
type EditableUser = Mutable;

const draftUser: EditableUser = {
id: “usr_001”,
name: “山田 太郎”,
email: “yamada@example.com”,
};

// おお、エラーにならずに書き換えができるぞ!
draftUser.name = “山田 次郎”;

/

  • 実践Tips 2: 特定のプロパティだけを readonly にする、あるいはその逆
  • ユーティリティ型と組み合わせた高度なテクニック

/

// 逆に、通常のオブジェクトから特定のキーだけをreadonlyに強制したい場合などにも応用可能
type DeepPartialWithReadonly = {
readonly [K in keyof T]?: T[K] extends object ? DeepPartialWithReadonly : T[K];
};

どうだ? `-readonly` を使うことで、ガチガチに固められた `User` 型から自由を手に入れられたはずだ。実務では、ReduxのStateやReact Hook Formの型定義を調整する時によくこのパターンを使う。

—

3. シニアが教える「現場の罠」とベストプラクティス

このマップ型と `readonly` を使いこなす上で、実務でハマりがちな罠をいくつか共有しておこう。

罠1: ネストしたオブジェクト(深部)には効かない

先ほどのコードでサラッと流したが、マップ型はデフォルトではシャロー(浅い階層)にしか効かない。
`User` 型の中にさらにオブジェクト(例: `address: { readonly city: string }`)が含まれている場合、トップレベルの `Mutable` や `Readonly` を使っても、内側のプロパティのイミュータビリティまでは剥がせない(あるいは付与できない)。

対策:
もし深い階層(Deep)まで制御したい場合は、再帰的な型定義(Recursive Mapped Types)を書く必要がある。

// 再帰的に readonly を剥ぎ取る DeepMutable
type DeepMutable = {
-readonly [K in keyof T]: T[K] extends object ? DeepMutable : T[K];
};

ここまで書ければ、コードレビューでチームメンバーから「おっ、こいつ分かってるな」と一目置かれること間違いなしだ。

罠2: 組み込み型(Utility Types)との車輪の再発明に注意

TypeScriptには標準で `Readonly` が用意されている。だから、わざわざ自分で `readonly [K in keyof T]: T[K]` と書く必要はあまりない。
ただし、今回紹介した `-readonly` を使った「読み取り専用を解除する `Mutable`」のような型は標準ライブラリには(バージョンによっては)標準搭載されていないことが多いので、プロジェクトの共通型定義ファイル(`types/utils.d.ts` など)に持たせておくと非常に重宝する。

—

まとめ

TypeScriptのマップ型における `readonly` の付与(`+readonly` / `readonly`)と削除(`-readonly`)は、型を自在に変形させるための必須スキルだ。

  • `readonly` で意図せぬ破壊的変更を防ぐ。
  • `-readonly` で、サードパーティの制約やイミュータブルな型から一時的に自由を奪還する。

フロントエンドの規模が大きくなればなるほど、データの「書き込める場所」と「書き込んではいけない場所」の境界線を型で明確に引くことが、バグを防ぐ最大の防御壁になる。
今日の帰りにでも、自分が関わっているプロジェクトの型定義を見直して、「あ、ここは `-readonly` が使えそうだな」という箇所を探してみてくれ。

質問があったらいつでも声をかけてくれ。一緒に最高のコードベースを作っていこうぜ。

コメント

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