【実務・中級編】 マップ型(Mapped Types)の基礎 – TypeScript実践ガイド

こんにちは。チームのコードレビューをしていて、「お、ここ綺麗に型がまとまってるね」と後輩の成長に嬉しくなったり、逆に「あー、ここコピペで同じような型定義を量産しちゃってるな…」と頭を抱えたりするシニアエンジニアの私です。

君たち、日々のフロントエンド開発でお疲れ様です。APIから返ってくるレスポンスの型定義や、フォームの入力状態を管理する型を作るときに、同じようなプロパティ名と型を何度も手で書いて疲れていませんか?「あっちの型が変わったら、こっちの型も手動で修正して…」なんてやっていたら、いつか痛い目を見る。というか、バグの温床になります。

今回は、そんな退屈で泥臭い作業をエレガントに消し去る、TypeScriptの奥義「マップ型(Mapped Types)」の基礎について、実務の現場目線でみっちり解説していこう。

—

1. マップ型(Mapped Types)とは何か? なぜ現場で必要なのか

一言で言えば、マップ型とは「既存の型をベースにして、プロパティをループ(反復)させながら新しい型をゴリッと生成する仕組み」だ。JavaScriptの `Array.prototype.map()` の型バージョンだと思えばいい。

中級へのステップアップとして、君たちにまず意識してほしいのは「DRY(Don’t Repeat Yourself)原則の型への適用」だ。
例えば、ユーザー情報を表す型がすでに存在するとしよう。

type User = {
id: string;
name: string;
age: number;
};

ここで、画面の入力フォーム用に「すべての項目をオプショナル(省略可能)にしたい」、あるいは「すべての項目を読み取り専用(readonly)にしたい」という要件が降ってきたとする。
まさか、こんな風に手で書き直したりしてないだろうね?

// ❌ やってはいけないアンチパターン:手動で複製する
type UserFormInput = {
id?: string;
name?: string;
age?: number;
};

これだと、元々の `User` 型の `age` が `number | null` に変わった瞬間、この `UserFormInput` の修正を忘れてバグる。こういう「人間の手による同期」は、フロントエンド開発において最も排除すべき不確実性だ。

ここでマップ型の登場だ。TypeScriptの標準ライブラリには、これを秒で解決する `Partial` というユーティリティ型があらかじめ用意されている。実は、これこそがマップ型で作られている代表例なのだ。

—

2. マップ型の基本構文:裏側では何が起きているのか?

百聞は一見に如かず。まずは、標準の `Partial` が内部でどう書かれているか、その自前実装(カスタムマップ型)を見てみよう。

/

  • Tのすべてのプロパティをオプショナルにするマップ型
  • [K in keyof T] がまさにループ処理の正体

/
type MyPartial = {
[K in keyof T]?: T[K];
};

// 使い方
type User = {
id: string;
name: string;
age: number;
};

type CustomPartialUser = MyPartial;
/

  • 生成される型:
  • {
  • id?: string;
  • name?: string;
  • age?: number;
  • }

/

この構文の肝を分解して説明するぞ。

1. `keyof T`
これは、型 `T` が持つすべてのプロパティ名をユニオン型として抽出しろ、という命令だ。(上の例なら `”id” | “name” | “age”`)
2. `[K in keyof T]`
これがマップ型の核心。JavaScriptの `for…in` や `Object.keys()` のように、ユニオン型から1つずつキーを取り出して変数 `K` に代入し、新しい型のプロパティを再構築していく。
3. `?:` と `T[K]`
キー `K` に対してオプショナル (`?`) を付与し、元の型が持つプロパティの型(ルックアップ型 `T[K]`)をそのまま割り当てている。

💡 そもそも、TypeScriptのコンパイラは裏側でどう処理しているのか?

ブラウザのJavaScriptエンジン(V8など)が実行する前段階、つまりTypeScriptのコンパイラ(tsc)が型チェックを行うビルドの裏側で、このマップ型は静的に展開(評価)されている。

コンパイラは `keyof T` を見た瞬間、メモリ上でそのオブジェクトのキーのリストをスキャンし、`[K in …]` のループをメタプログラミング的に回して、新しい抽象構文木(AST)上の型定義を構築する。
つまり、ランタイム(実行時)のパフォーマンスには一切影響を与えない。TypeScriptはコンパイル時にすべてプレーンなJavaScriptにトランスパイルされ、型情報は綺麗に消え去るからだ。安心してガンガン使っていい。

—

3. 現場で即コピペして使える!実践的なマップ型の活用パターン

基本が分かったところで、実務のフロントエンド開発で「おっ、こいつ分かってるな」と思われる実践的なパターンを3つ紹介しよう。

パターンA:すべてのプロパティを `readonly`(読み取り専用)にする

状態管理(ReduxやZustand、あるいはReactのContextなど)で、イミュータブル(不変)なデータを扱うときに重宝する。これもTypeScript標準の `Readonly` の中身だ。

type MyReadonly = {
readonly [K in keyof T]: T[K];
};

type Config = {
endpoint: string;
timeout: number;
};

type ImmutableConfig = MyReadonly;
/

  • 生成される型:
  • {
  • readonly endpoint: string;
  • readonly timeout: number;
  • }

/

const appConfig: ImmutableConfig = {
endpoint: ‘https://api.example.com’,
timeout: 5000,
};

// ❌ エラー!読み取り専用なので再代入できない
// appConfig.timeout = 10000;

パターンB:修飾子(Modifiers)の除去(`-?` と `-readonly`)

逆に、元からオプショナルになっているプロパティや `readonly` なプロパティから、その制約を剥ぎ取ることもできる。これがマップ型の面白いところだ。マイナス記号(`-`)をつけるだけ。

// すべてのプロパティを必須(Required)にする標準ユーティリティ Required の仕組み
type MyRequired = {
[K in keyof T]-?: T[K]; // `-?` でオプショナルを強制解除!
};

type PartialUser = {
id?: string;
name?: string;
};

type StrictUser = MyRequired;
/

  • 生成される型:
  • {
  • id: string;
  • name: string;
  • }
  • (両方とも必須プロパティに強制変換される)

/

パターンC:キーのリッピング(再マッピング:`as` 句の活用)

TypeScript 4.1以降で導入された神機能「Template Literal Types」と「Key Remapping (`as`)」の組み合わせだ。これを知っていると、中級から一気に上級アーキテクトに近づける。

例えば、APIのデータモデル(snake_case)を、フロントエンドのフォーム状態(`on[FieldName]Change` みたいなイベントハンドラの型)に一括変換したいとき:

// 既存のデータモデル
type UserProfile = {
first_name: string;
last_name: string;
};

// キーを “on” + Capitalize + “Change” に変換しつつ、値はコールバック関数にする!
type FormHandlers = {
[K in keyof T as `on${Capitalize}Change`]: (value: T[K]) => void;
};

type UserProfileHandlers = FormHandlers;
/

  • 生成される型:
  • {
  • onFirst_nameChange: (value: string) => void;
  • onLast_nameChange: (value: string) => void;
  • }

/

どうだ、鳥肌が立たないか?
このように、マップ型は単にプロパティのコピーを作るだけでなく、キーの名称そのものを動的にトランスフォームすることができるのだ。

—

4. シニアからのアドバイスとまとめ

マップ型を実務で使いこなすための心構えを最後に一つ。

「車輪の再発明」は学習には最高だが、実務ではまずTypeScriptが標準で提供しているユーティリティ型(`Partial`, `Required`, `Readonly`, `Record`, `Pick`, `Omit` など)を使い倒すこと。これらはすべて、今回解説したマップ型と条件付き型(Conditional Types)の組み合わせでできてきている。

標準機能で物足りない、あるいはドメイン固有の複雑な型変換(例えばすべての値を `Promise` でラップしたAPIクライアント用型など)が必要になったときに、今回学んだマップ型の基本構文を思い出して自作してほしい。

型定義は、単なるエラーチェックの道具じゃない。「チーム全員に対する、最高にわかりやすいコードの契約書(ドキュメント)」だ。
マップ型を武器にして、冗長で保守性の低い型定義とは今日でおさらばしよう。

それじゃあ、次のタスクもスマートに片付けていこうぜ。

コメント

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