【実務・中級編】 Mapped Typesの基本構文と再利用 – TypeScript実践ガイド

こんにちは。フロントエンドチームのシニアアーキテクトだ。
最近、君たちの書くコードをレビューしていて、「おっ、いい感じに型定義を使いこなせるようになってきたな」と感心することが増えた。だがな、APIから返ってくる巨大なJSONの型定義を前にして、ひたすら手作業でプロパティを書き換えたり、同じようなオブジェクト型を何個も重複して定義したりしていやしないか?

「この型、ぜんぶオプショナル(`?`)にしたいだけなのに、また同じプロパティを並べるの……?」
そんな絶望を抱えた夜があるなら、今日で終わりだ。

今回は、TypeScriptの中級から上級へステップアップするための最大の関所であり、最強の武器でもある 「Mapped Types(マッピングされた型)」 について徹底的に解説しよう。この扉を開ければ、君の型定義は「静的な設計図」から「動的に変化するメタプログラミングのキャンバス」へと生まれ変わる。

—

Mapped Typesとは何か?なぜ現場で不可欠なのか

一言で言えば、Mapped Typesとは 「既存のオブジェクト型をベースにして、新しいオブジェクト型を自動生成する仕組み」 だ。 JavaScriptの `Array.prototype.map()` を思い浮かべてほしい。あれの「型版」だと思えばいい。

実務でなぜこれが不可欠か?
例えば、サーバーからユーザー情報(`User`)が飛んできたとする。画面のフォームを実装するとき、最初はすべての項目が空(または部分的な更新)だから、すべてのプロパティをオプショナルにしたい。さらに、バリデーション中や送信中には、勝手に値を書き換えられないように `readonly` にしたい。

ここで素朴なジュニアエンジニアは、こう書く。

// ❌ 現場で絶対にやってはいけない「コピペ地獄」
interface User {
id: string;
name: string;
email: string;
}

interface EditableUser {
id?: string;
name?: string;
email?: string;
}

interface ReadonlyEditableUser {
readonly id?: string;
readonly name?: string;
readonly email?: string;
}

おいおい、`User` のプロパティが変更されるたびに、下の2つのインターフェースも手動で修正するのか? 変更漏れが起きてバグの温床になるのが目に見えているよな。

これをスマートに、かつ完全な型安全を保って一撃で解決するのが Mapped Types だ。

—

基本構文の解剖:`[K in keyof T]` の正体

まずは、Mapped Types の基本形を見てみよう。

type OptionsFlags = {
[K in keyof T]: boolean;
};

この呪文のような構文を、上から順に紐解いていこう。

1. `keyof T`
これは、型 `T` が持つすべてのプロパティ名を「ユニオン型」として抽出する演算子だ。例えば `T` が `{ id: string; name: string }` なら、`keyof T` は `”id” | “name”` になる。
2. `K in …`
JavaScriptの `for…in` や `for…of` のようなものだ。抽出されたユニオン型(`”id” | “name”`)から、1つずつプロパティ名を取り出して変数 `K` に代入している。
3. `[K in keyof T]: …`
新しいオブジェクト型を作るときに、取り出した `K` を新しいキーとして再構築している。

つまり、先ほどの `OptionsFlags` に `User` 型を渡すと、TypeScriptの裏側では以下のような変換が自動で行われていることになる。

// OptionsFlags が展開されるとこうなる
{
id: boolean;
name: boolean;
}

どうだ? 泥臭い手作業がいかに無駄だったか、分かってもらえたはずだ。

—

修飾子のコントロール:modifiers(`+`, `-`, `readonly`, `?`)

Mapped Types の真骨頂は、単にキーをループさせるだけではない。プロパティに付いている 修飾子(`readonly` や `?`)の付与と削除 を自由自在にコントロールできる点にある。

ここで、TypeScriptが裏側でどう処理しているかという少しマニアックな話をしよう。
TypeScriptのコンパイラは、型を評価する際、各プロパティに対して「このキーは読み取り専用か?」「省略可能か?」というフラグを持たせている。Mapped Typesでは、このフラグに対して プリフィックス(`+` または `-`) をつけて制御できるんだ。

1. 修飾子の付与(`+` または省略)

何もつけないか、明示的に `+` をつけると、修飾子を「追加」できる。

type CreateMutable = {
-readonly [K in keyof T]: T[K]; // readonly を剥ぎ取る!
};

2. 修飾子の削除(`-`)

これが実務でめちゃくちゃよく使う。`-` をつけることで、既存の型についている修飾子を「強制的に剥ぎ取る」ことができる。

実務で頻出する「部分的な更新(Partial)」と「全項目の必須化(Required)」の自前実装を見てみよう。

// すべてのプロパティをオプショナル(?付き)にする(標準の Partial と同じ)
type MyPartial = {
[K in keyof T]?: T[K]; // 暗黙的に +? が適用されている
};

// 【重要】逆に、オプショナルをすべて剥ぎ取って必須(Required)にする
type MyRequired = {
[K in keyof T]-?: T[K]; // `-?` で「オプショナルを外す」!
};

// 【重要】readonlyをすべて剥ぎ取って書き込み可能にする
type MyMutable = {
-readonly [K in keyof T]: T[K]; // `-readonly` で読み取り専用を外す!
};

この `-?` や `-readonly` は、ライブラリの型定義をハックするときや、厳密なドメインモデルを構築する際に知っていないと絶対に書けないテクニックだ。覚えておいて損はない。

—

現場で即戦力になる!実践的なコード例

理屈はこれくらいにして、明日からそのままプロジェクトにブチ込める実用的なサンプルコードを用意した。

テーマは 「APIレスポンス型から、フォームの状態管理用型を生成するユースケース」 だ。

/

  • 1. サーバーから取得するユーザーのベース型

/
interface ApiUser {
readonly id: number;
name: string;
email: string;
age?: number; // 最初からオプショナルな項目もある
}

/

  • 2. フォーム入力用型への変換ロジック(Mapped Typesの応用)
  • – すべてのプロパティを書き込み可能にする (-readonly)
  • – すべてのプロパティをフォーム入力用にオプショナルにする (-? ではなく付与する)
  • – 値の型を string に統一する(フォームの入力値は基本stringのため)

/
type FormValues = {
-readonly [K in keyof T]: string | undefined;
};

/

  • 【結果の型】
  • type UserFormState = {
  • id: string | undefined;
  • name: string | undefined;
  • email: string | undefined;
  • age: string | undefined;
  • }

/
type UserFormState = FormValues;

// — 実際のコンポーネントでの使用イメージ —

const initialFormState: UserFormState = {
id: “1”,
name: “Yamada Taro”,
email: “yamada@example.com”,
age: undefined, // フォームの初期値として安全に扱える
};

// 仮に id を書き換えようとしても、-readonlyのおかげでコンパイルエラーにならない!
// ※実務ではフォームの状態管理ライブラリ(React Hook Form等)のジェネリクスと組み合わせると神がかり的に機能する

このように、APIの型(`ApiUser`)を正として、そこから派生するUI用の型をMapped Typesで自動生成しておけば、APIの仕様変更(例: `phone` プロパティの追加など)があった瞬間、UI側のフォーム型にも自動で反映される。これぞ保守性の高い、プロのフロントエンドアーキテクチャだ。

—

シニアからのアドバイス:やり過ぎには注意しろ

Mapped Typesは強力すぎるゆえに、チーム開発で乱用すると「今、目の前にある型が一体どういう構造になっているのか、パッと見で分からない(型迷子)」という状態を引き起こす。

初心者がやりがちなアンチパターンとして、Mapped Typesを何重にもネストさせたり、複雑な条件分岐(Conditional Types)と組み合わせすぎて、エラーメッセージがエディタの画面を埋め尽くすようなコードを書くケースがある。

「コードは読まれやすさが正義」 だ。複雑なMapped Typesを作る場合は、必ず適切な名前(エイリアス)をつけ、JSDocで「この型が何をするものなのか」をチームメンバーに向けて日本語で残しておくこと。

Mapped Typesをマスターした君なら、もう「型の重複定義」に怯える必要はない。
さあ、明日からのコードレビューで、泥臭い手書きの型を華麗にリファクタリングして見せてくれよ!

コメント

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