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

やあ。最近、チームのコードレビューをしていて「あ、ここはMapped Types使えばボイラープレートをごっそり削れるのになぁ」って思う場面にめちゃくちゃ遭遇するんだよね。

手動で似たような型を何個も定義して、APIの仕様変更のたびに何箇所も手直しして……。そんな泥臭い作業、もう終わりにしよう。TypeScriptの中級を抜け出して「本当の意味での型職人」になるためのパスポート、それが Mapped Types(マップ型) だ。

今日は、公式ドキュメントの薄っぺらい解説の先にある、実務の現場で明日から爆速で使える実践的なノウハウをすべて叩き込んでいくよ。コーヒーでも飲みながら、じっくりついてきてほしい。

—

1. Mapped Typesとは何か?(基本のキと、その裏側)

一言で言えば、Mapped Typesは「既存の型をベースにして、プロパティをループ(反復処理)しながら新しい型を生成する仕組み」だ。JavaScriptで言う `Array.prototype.map()` の型版だと思ってもらえれば、イメージしやすいはずだ。

基本構文はこうだ:

type MappedType = {
[K in keyof T]: 処理;
};

ここで何が起きているか、TypeScriptのコンパイラ(ブラウザやNode.jsの上で動くTypeScriptの静的解析エンジン)の気持ちになって考えてみよう。

TypeScriptのコンパイラは、コードを機械語にコンパイルする前に「型チェック」という途方もない計算を行っている。`keyof T` でオブジェクト型 `T` のすべてのキーをユニオン型として抽出(例:`’id’ | ‘name’ | ‘email’`)し、`in` 演算子でそのユニオン型を一つずつイテレート(ループ)して新しいオブジェクトのキーとバリューの型を組み立て直しているんだ。

この「型の世界でのメタプログラミング」ができるようになった瞬間から、君のコードベースの保守性は劇的に跳ね上がる。

—

2. 現場で即戦力になる!実用コードパターン

机上の空論はこれくらいにして、実際に現場でよくあるユースケースを見ていこう。コピペしてすぐにでもプロジェクトに導入できるコードを用意したよ。

パターンA:すべてのプロパティをオプショナル(任意)にする

ReduxやZustandなどの状態管理、あるいはフォームの更新処理(Patchリクエスト)で、既存のエンティティの一部だけを渡したいときってよくあるよね。標準ライブラリには `Partial` として用意されているけれど、自作してみると仕組みがよくわかる。

/

  • ユーザー情報のマスター型

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

/

  • 【自作版】すべてのプロパティをオプショナルにするマップ型
  • K in keyof T でキーを全走査し、後ろの `?` でオプショナルに変換している

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

// 使用例:更新APIに送るデータは一部だけでもOKになる
const updateData: CustomPartial = {
name: “新しい名前”,
// id, email, age は書かなくてもエラーにならない!
};

パターンB:読み取り専用(Readonly)の壁を作る

ドメイン駆動設計などで、不変(Immutability)を担保したいドメインモデルを扱うとき。うっかり書き換えられてバグるのを型の力で完全に封じ込める。

/

  • 【自作版】すべてのプロパティを読み取り専用にするマップ型

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

const config: CustomReadonly<{ endpoint: string; timeout: number }> = {
endpoint: “https://api.example.com”,
timeout: 3000,
};

// ⚠️ ここで値を書き換えようとすると、TypeScriptのコンパイラが怒ってくれる
// config.timeout = 5000; // ⚡️ Error: Cannot assign to ‘timeout’ because it is a read-only property.

—

3. シニアが教える「プロの技」:Modifiers(修飾子)と Remapping (`as`)

ここからが本番だ。中級から一段上のステージへ上がるための、少し高度で実務に直結するテクニックを教えよう。

1. 修飾子の除去(`-` プレフィックス)

もし、ベースとなる型が最初からオプショナルだらけだったり、Readonlyだらけだったらどうする?
TypeScriptでは、修飾子の前に `-`(マイナス)をつけることで、「その修飾子を剥ぎ取る(強制的に必須・書き込み可能にする)」ことができるんだ。

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

/

  • すべてのオプショナルを剝ぎ取り、必須(Required)にするマップ型

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

// 使用例:すべてのプロパティが必須になり、undefinedを許さなくなる
const strictUser: ForceRequired = {
id: “123”,
name: “太郎”, // どちらも欠けているとエラーになる
};

この `-?` や `-readonly` は、外部ライブラリの型をハックして強引に自分たちの規律に合わせるときによく使う、シニアの隠し味みたいなものさ。

2. キーの再マッピング (`as` 構文)

TypeScript 4.1以降で導入された `as` キーワードを使うと、ループしながらキーの名前自体を変形することができるようになった。これ、マジで神機能だから覚えておいて。

例えば、APIから返ってきたスネークケースのキーを、フロントエンド側で使うキャメルケースのイベントハンドラ風(`onXxx`)の型に変換したいとき:

// APIのレスポンス(スネークケース)
type ApiResponse = {
user_id: string;
first_name: string;
};

/

  • キーのプレフィックスに “get” を付与しつつ、
  • テンプレートリテラル型と組み合わせてキー名を自由に変形する例

/
type Getterify = {
[K in keyof T as `get${Capitalize}`]: () => T[K];
};

// 変換された型:
// {
// getUser_id: () => string;
// GetFirst_name: () => string; …おっと、これだと大文字小文字が気になるね
// }

テンプレートリテラル型(`Template Literal Types`)と組み合わせることで、型の世界で文字列操作が自由自在にできるようになる。ここをマスターすると、バックエンドのいびつなスキーマにフロントエンド側で悩まされることが激減するよ。

—

4. 現場での注意点:やりすぎはコードリーダビリティの毒

ここまでMapped Typesの素晴らしさを語ってきたけれど、最後に一つだけシニアとして苦言を呈しておきたい。

「型パズルに走りすぎて、読めないコードを書くな」

Mapped TypesやConditional Types(条件付き型)を何重にもネストさせると、確かに「俺、すげえTypeScript書いてるぜ…!」っていう陶酔感に浸れる。でもね、半年後の君や、新しくチームに入ってきたメンバーがそのコードを見たとき、「なんだこれ、解読に30分かかるぞ」って絶望することになるんだ。

型はあくまで「バグを早期に検出し、開発体験(DX)を爆上げするためのツール」であって、パズルを解くためのオモチャじゃない。
複雑なMapped Typesを作る場合は、必ずコメントで「何をやっている型なのか」を日本語で書き残すか、あるいは素直に公式のユーティリティ型(`Partial`, `Required`, `Record`, `Pick`, `Omit` など)で代替できないかをまず検討してほしい。

—

まとめ

  • Mapped Typesとは: オブジェクトのキーを `in keyof T` でループして新しい型を生成する強力な仕組み。
  • 基本の応用: `?` や `readonly` を付けたり、逆に `-?` や `-readonly` で剥ぎ取ったりできる。
  • 発展形: `as` 構文を使ったキー名の動的変更やテンプレートリテラル型との合わせ技で、どんなAPI仕様にも型レベルで適応できる。
  • 教訓: 力には責任が伴う。チーム全員が理解できる範囲の美しさを心がけよう。

さあ、今日の業務から、無駄な手書きの型定義はすべてMapped Typesでスマートにリファクタリングしてみよう。君のコードレビューが楽しみだな!

コメント

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