こんにちは。フロントエンドのアーキテクチャの荒波をくぐり抜け、日々V8エンジンの機嫌とTypeScriptの型チェッカーの機嫌を伺いながらコードを書いている者です。
型システムというのは不思議なもので、最初は「安全性を担保する単なるコンパイル時のガードレール」だと思って侮っていると、プロジェクトが巨大化した瞬間に牙を剥いてきます。特に、APIレスポンスの形が刻々と変わり、ReduxやZustandといった状態管理のレイヤーでイミュータビリティを強制されるモダンなWebアプリ開発において、型変形(Type Transformation)のスキルはもはやシニアの必須教養です。
今回は、その中でもMapped Typesにおける修飾子操作――すなわち `readonly` と `?`(オプショナル)の付与と削除(`+`, `-` 修飾)について、実務で本当に役立つアーキテクチャの視点から深掘りしていきましょう。
—
なぜ「型の修飾子操作」がシニアの武器になるのか
大規模なフロントエンド開発において、最も避けるべき悪夢は何でしょうか?
それは、「データのライフサイクルごとに似たような型を何十個も定義し、バックエンドのスキーマ変更のたびに手動で追従させ、気がついたらコードベースが型定義のゴミ屋敷と化している」という状況です。
例えば、データベースから取得した厳格なエンティティ型(すべて `readonly` で必須プロパティ)があったとします。これをフォームの入力状態(すべてミュータブルで、かつ未入力があり得るためオプショナル)に変換したいとき、あなたならどうしますか?まさか手動で全部書き直したりしていませんよね?
ここで登場するのが、Mapped Typesにおける `+` と `-` の修飾子操作です。これらを使いこなすことで、単一の信頼できる情報源(Single Source of Truth)から、あらゆるレイヤーの型を自動生成できるようになります。
基礎の復習:`+` と `-` のメカニズム
TypeScriptの Mapped Types では、修飾子の前に明示的に符号をつけることができます。
- `+readonly` または単に `readonly`:プロパティを読み取り専用にする
- `-readonly`:プロパティの読み取り専用を解除し、ミュータブルにする
- `+?` または単に `?`:プロパティをオプショナルにする
- `-?`:プロパティのオプショナルを外し、必須(Required)にする
言葉で聞くよりも、実際のコードを見た方が早いでしょう。まずは、この修飾子操作がどのように型をねじ曲げるのか、実務的なユースケースベースで見ていきます。
実践:ReadOnlyなAPIレスポンスから、自由にいじれるフォーム用型への錬成
以下のコードを見てください。バックエンドから送られてくる厳格なユーザーエンティティを想定しています。
// バックエンドから返される、変更不可(readonly)かつ全項目必須のドメインモデル
type UserEntity = {
readonly id: string;
readonly email: string;
readonly role: ‘admin’ | ‘user’;
};
/
- 【アーキテクチャ上の課題】
- このUserEntityをそのまま画面のフォーム状態(React Hook Formなど)に持ち込むと、
- 「途中段階ではメールアドレスが空かもしれない(オプショナル)」かつ
- 「ユーザーが値を書き換えられる(ミュータブル)」必要がある。
/
// 修飾子を華麗に操作して、フォーム用の型を自動生成する
type FormState
// -readonly で読み取り専用を剥ぎ取り、+? でオプショナルにする
-readonly [K in keyof T]?: T[K];
};
type UserFormState = FormState
/
- 生成される UserFormState の実態:
- {
- id?: string;
- email?: string;
- role?: ‘admin’ | ‘user’;
- }
/
このアプローチの美しいところは、`UserEntity` に新しいフィールド(例えば `readonly lastLoginAt: Date`)が追加された瞬間、何もしなくても `UserFormState` が自動的に追従する点です。メンテナンスコストは劇的に低下します。
—
パフォーマンスとメモリ効率、そして型チェッカーの負荷
ここで、少し低レイヤーな視点――TypeScriptコンパイラ(tsc)の内部挙動とパフォーマンスの話をしましょう。
「型定義なんてコンパイルが終われば消えるんだから、パフォーマンスに関係ないだろう」と思っていませんか?
実は、過剰に複雑なMapped Typesや、条件付き型(Conditional Types)のネストは、VS Codeのインテリセンス(Language Server)を重くし、CI環境での型チェック時間を数分単位で引き延ばす原因になります。
型の評価コストを抑える設計
TypeScriptの型チェッカーは、Mapped Typesを評価する際、内部でシンボルのルックアップとユニオン型の分配を行います。
特に、既存のユーティリティ型(`Partial
// 悪い例:ユーティリティ型を無駄にネストさせている
type BadState
// 良い例:必要な修飾子の操作を1つのMapped Type内で完結させ、コンパイラの負荷を最小化する
type OptimizedState
-readonly [K in keyof T]?: T[K];
};
実務の現場では、数千行規模の巨大なスキーマを扱うことがあります。コンパイラのメモリ効率を保つためにも、修飾子操作はできる限りフラットなMapped Type内でワンパスで処理するのが、大規模フロントエンド開発におけるプロの技です。
—
発展編:条件付き型(Conditional Types)と組み合わせた動的モデリング
さらに高度なアーキテクチャを目指すなら、Mapped Types内での修飾子操作を、プロパティの「型そのもの」の条件分岐と組み合わせるテクニックが不可欠です。
例えば、「特定の型を持つプロパティだけをミュータブルにし、それ以外は `readonly` のまま維持したい」といった、ドメイン駆動設計(DDD)における厳格な不変条件(Invariants)を強制したい場面を想像してください。
// 特定のプレフィックスを持つキー、あるいは関数型だけを操作する高度なMapped Type
type MutableNonFunctionKeys
-readonly [K in keyof T as T[K] extends Function ? never : K]: T[K];
};
type ComplexDomainModel = {
readonly id: string;
readonly version: number;
readonly save: () => void; // メソッド
};
type EditableFields = MutableNonFunctionKeys
/
- 結果:
- {
- id: string; // -readonly が適用され、ミュータブルになる(関数ではないため)
- version: number; // 同上
- // save メソッドは ‘as never’ によって型から消え去る
- }
/
この `as` 節(Key Remapping)と修飾子操作(`-readonly`)のコンビネーションは、フレームワークの内部実装や、高度なライブラリの型設計(ZodやValibot、あるいは独自のRPCクライアントなど)において、ボイラープレートを消し去るための強力な武器になります。
—
現場でやりがちなアンチパターンと回避策
最後に、実務でこの機能を使う際によく見落としがちな「落とし穴」について共有しておきます。
1. `Partial` や `Required` の標準実装への過度な依存
TypeScript標準の `Partial
意図しないミュータブル化を防ぐために、「既存の `readonly` を維持したままオプショナルにしたいのか」「`readonly` ごと剥がしたいのか」を意識し、標準ユーティリティを盲信せずカスタムMapped Typeを自製する勇気を持ちましょう。
2. オプショナル(`?`)と `undefined` の混同
`{ foo?: string }` と `{ foo: string | undefined }` は、型システム上は似て非なるものです。前者は「プロパティ自体が存在しなくてもよい(keyがundefined)」ですが、後者は「プロパティの存在は必須だが、値がundefinedである」ことを意味します。
Mapped Typesで `+?` を付与する際は、キーの存在有無がどう変わるのか、ランタイムのシリアライゼーション(JSON.stringifyなど)にどのような影響を与えるかまで想像を巡らせてください。
—
まとめ
TypeScriptの Mapped Types における `readonly` と `?` の操作(`+`, `-`)は、単なるシンタックスシュガーではありません。
それは、「単一のドメインモデルから、フロントエンドのあらゆるレイヤー(API、状態管理、UIフォーム、永続化)の型を完璧に導出するための、コンパイラをコントロールする権限」 です。
泥臭い手動の型定義や、コピペによる型の量産とは今日で決別し、型システムをあなたの意のままに飼い慣らした堅牢なアーキテクチャを構築してください。それでは、良きTypeScriptライフを。

コメント