TypeScript Mapped Types完全ガイド〜in keyof・Key Remapping・実用パターン20選【2026年版】〜

TypeScriptのUtility Typesを使っているうちに「これ、内部はどうやって実装されているんだろう?」と気になったことはありませんか。答えはMapped Types(マップ型)です。Partial<T>Readonly<T>Pick<T,K>も、すべて[K in keyof T]というたった1行のMapped Typesで書かれています。

本記事はTS 5.x準拠でMapped Typesの内部実装に踏み込む実装特化ガイドとして、50個以上のコピペで動くコードとともに、自作Utility Typesの作り方・Key Remapping・実用パターン20選を解説します。Mapped Typesを使いこなせるようになれば、any撲滅・型重複の一掃・APIスキーマからの型生成まで、TypeScriptの型表現力が一気に跳ね上がります。

この記事で得られること

  • Mapped Typesの基本構文[K in keyof T]: T[K]の完全理解
  • Partial / Required / Readonly / Pick / Omit / Recordの自作実装
  • + / – modifier、Key Remapping (as) によるキー変換テクニック
  • DeepPartial / DeepReadonly / PickByValue など実用型20パターン
  • EventHandler型生成・APIパス→ハンドラ型・Form型生成などの実戦応用
  1. Mapped Typesとは何か
    1. 素朴な型定義との比較
    2. 構文の分解
  2. 基本のマッピング
    1. 恒等写像としてのIdentity型
    2. keyofとインデックスアクセス型のおさらい
  3. 標準Utility Typesを自作する
    1. MyPartial: 全プロパティを optional に
    2. MyRequired: 全プロパティを必須に
    3. MyReadonly: 全プロパティをreadonlyに
    4. MyMutable: readonlyを剥がす
    5. MyPick: 指定キーだけ抽出
    6. MyOmit: 指定キーを除外
    7. MyRecord: キー集合と値型からオブジェクト型を構築
  4. + / – modifierを使いこなす
    1. + modifier(明示的に追加)
    2. – modifier(剥がす)
    3. modifierの効きどころ早見表
  5. Key Remapping (as 句)
    1. 基本のas構文
    2. キー除外: as でneverを返すと消える
    3. プレフィックスを付けてキー変換
    4. Getter/Setter自動生成型
  6. 実用パターン20選
    1. パターン1: EventHandler型の自動生成
    2. パターン2: Redux風Action型生成
    3. パターン3: APIパスからハンドラ型を作る
    4. パターン4: Formフィールド型生成
    5. パターン5: Database スキーマ → 型
    6. パターン6: union → object 変換
    7. パターン7: tuple → object 変換
    8. パターン8: 階層的Mapped Type
    9. パターン9: Distributive Conditional Typesとの組み合わせ
    10. パターン10: Recursive Mapped Types
    11. パターン11: DeepPartial
    12. パターン12: DeepReadonly
    13. パターン13: DeepRequired
    14. パターン14: PickByValue
    15. パターン15: OmitByValue
    16. パターン16: NullableAll
    17. パターン17: NotNullableAll
    18. パターン18: 関数だけ抜き出すFunctionProperties
    19. パターン19: プロパティを全て関数化(Promise版)
    20. パターン20: 列挙型からswitchハンドラ要求型
  7. ライブラリでのMapped Types実装例
    1. GraphQL Codegen風のレスポンス型生成
    2. Prisma風のSelect型生成
    3. tRPC風のクライアント型生成
  8. パフォーマンスと落とし穴
    1. 落とし穴1: 再帰の深さ制限
    2. 落とし穴2: 巨大ユニオンに対するMapped Types
    3. 落とし穴3: as nevernで消したつもりが残る
    4. 落とし穴4: 推論を阻害するMapped Types
  9. Before / After リファクタリング集
    1. Before/After 1: APIレスポンスの全フィールドnullable
    2. Before/After 2: Form状態
    3. Before/After 3: Reduxアクション
    4. Before/After 4: Getter群
    5. Before/After 5: PATCH用のPartial DTO
  10. 収益化のための学習ロードマップ
  11. まとめ: Mapped Typesは「型のfor文」

Mapped Typesとは何か

Mapped Types(マップ型)は、既存の型のプロパティを一括変換して新しい型を作る仕組みです。TypeScript 2.1で導入されて以来、Utility Typesの土台として欠かせない存在になっています。

素朴な型定義との比較

たとえばUser型の全プロパティをoptionalにしたい場合、Mapped Typesを使わずに書くと以下のようになります。

// ❌ Mapped Typesを使わない: プロパティが増えるたび修正が必要
type User = {
  id: string;
  name: string;
  email: string;
  age: number;
};

type OptionalUser = {
  id?: string;
  name?: string;
  email?: string;
  age?: number;
};

Mapped Typesを使うと、1行で同じ意味の型が書けて、Userの定義変更が自動でOptionalUserに反映されます。

// ✅ Mapped Typesを使う: User変更時に自動追従
type User = {
  id: string;
  name: string;
  email: string;
  age: number;
};

type OptionalUser = {
  [K in keyof User]?: User[K];
};

構文の分解

Mapped Typesの基本構文は次の4要素で構成されます。順に意味を確認しておきましょう。

type Sample<T> = {
//                ↓ ①新しいプロパティキー名
  [K in keyof T]: T[K];
//   ↑ ②キーの集合     ↑ ③値の型
};
// ④ オプションでmodifier (readonly / ?) と as 句が付く

このうち最も重要なのはK in keyof Tの部分です。これはユニオン型を1つずつ取り出してキーに展開する繰り返し処理のような働きをします。配列のforEachに近いイメージで覚えると分かりやすいです。

基本のマッピング

まずは何も変換せず「元の型をそのままコピーする」だけのMapped Typeから始めます。これが全Utility Typesの出発点になります。

恒等写像としてのIdentity型

// 元の型と完全に同じ型を返すIdentity型
type Identity<T> = {
  [K in keyof T]: T[K];
};

type User = { id: string; name: string };
type SameAsUser = Identity<User>;
// = { id: string; name: string }

一見無意味に見えますが、Identity型はMapped Typesの動作を理解する最小単位です。「全キーを取り出して、値型をそのまま再代入する」が腹落ちすれば、あとはこのテンプレートを変形するだけです。

keyofとインデックスアクセス型のおさらい

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

// keyof T で全プロパティキーのユニオンが得られる
type UserKey = keyof User; // "id" | "name" | "age"

// T[K] でキーKに対応する値の型が得られる
type UserName = User["name"]; // string
type UserVal = User[keyof User]; // string | number

Mapped Typesは「keyof」でキー集合を作り、「T[K]」で値型を取り出すという二つの仕組みの組み合わせです。これさえ押さえれば、以降の応用パターンは全てこの延長線上で読み解けます。

標準Utility Typesを自作する

ここからはPartialPickなどの標準Utility Typesを、Mapped Typesで自作実装していきます。lib.es5.d.tsの中身を見ているのと同じ理解度に到達できます。

MyPartial: 全プロパティを optional に

// ?をキーの後ろに付けるとoptional
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

type User = { id: string; name: string; email: string };
type DraftUser = MyPartial<User>;
// = { id?: string; name?: string; email?: string }

標準のPartial<T>と完全に同じ型が手に入りました。フォーム入力中のドラフト状態など、「全部入っているとは限らない」型を表現するのに使います。

MyRequired: 全プロパティを必須に

// -? で optionalを剥がす(マイナスモディファイア)
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

type Draft = { id?: string; name?: string };
type Strict = MyRequired<Draft>;
// = { id: string; name: string }

-?はoptionalを取り除くマイナスモディファイアです。バリデーション後に「全部埋まっている」と保証された型を表現するときに使います。

MyReadonly: 全プロパティをreadonlyに

// readonly を付けるとプロパティが書き換え不可になる
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Config = { host: string; port: number };
type FrozenConfig = MyReadonly<Config>;

const c: FrozenConfig = { host: "localhost", port: 8080 };
// c.host = "127.0.0.1"; // ❌ Error: Cannot assign to 'host'

MyMutable: readonlyを剥がす

// -readonly で readonly を取り除く
type MyMutable<T> = {
  -readonly [K in keyof T]: T[K];
};

type Frozen = { readonly id: string; readonly name: string };
type Editable = MyMutable<Frozen>;
// = { id: string; name: string }

外部APIから受け取ったreadonly付きの型を内部で書き換え可能にしたい場合などに役立ちます。+/- modifierはMapped Typesの大きな武器なので、必ず使いこなせるようにしておきましょう。

MyPick: 指定キーだけ抽出

// Kはkeyof Tのサブセットに制限
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

type User = { id: string; name: string; password: string };
type PublicUser = MyPick<User, "id" | "name">;
// = { id: string; name: string }

K extends keyof T「Tに存在するキーしか選べない」制約を付けるのがポイントです。タイポを型エラーで弾けます。

MyOmit: 指定キーを除外

// ExcludeでKを除いたキー集合を作る
type MyOmit<T, K extends keyof any> = {
  [P in Exclude<keyof T, K>]: T[P];
};

type User = { id: string; name: string; password: string };
type SafeUser = MyOmit<User, "password">;
// = { id: string; name: string }

MyRecord: キー集合と値型からオブジェクト型を構築

// K側に固定ユニオン、V側に固定型を指定
type MyRecord<K extends keyof any, V> = {
  [P in K]: V;
};

type Role = "admin" | "editor" | "viewer";
type Permission = MyRecord<Role, boolean>;
// = { admin: boolean; editor: boolean; viewer: boolean }

標準のRecord<K,V>もまさにこの実装です。「キーが固定ユニオン、値が同じ型」のオブジェクトを作るときの定番パターンになります。

+ / – modifierを使いこなす

Mapped Typesではreadonly?に対して、明示的な+-のmodifierを付けられます。挙動を整理しておきましょう。

+ modifier(明示的に追加)

// + は付与を明示する(省略可能・readonly と + readonly は同義)
type AddReadonly<T> = {
  +readonly [K in keyof T]: T[K];
};

type AddOptional<T> = {
  [K in keyof T]+?: T[K];
};

– modifier(剥がす)

// - は取り除く(全modifierを一掃する型を作れる)
type StripModifiers<T> = {
  -readonly [K in keyof T]-?: T[K];
};

type Source = { readonly a?: string; readonly b?: number };
type Cleaned = StripModifiers<Source>;
// = { a: string; b: number }

外部ライブラリの型に余分なreadonly?が付いていて使いにくい場面では、このパターンで一掃できます。

modifierの効きどころ早見表

記法 意味 対応する標準Utility
readonly / +readonly readonlyを付与 Readonly<T>
-readonly readonlyを除去 (自作Mutable)
? / +? optionalを付与 Partial<T>
-? optionalを除去 Required<T>

Key Remapping (as 句)

TS 4.1で導入されたas句は、Mapped Typesのキー側を別の文字列に書き換える機能です。Mapped Typesの表現力を一気に押し上げた重要機能で、ここからが本領発揮になります。

基本のas構文

// 構文: [K in keyof T as NewKey]: ...
type Rename<T> = {
  [K in keyof T as `new_${string & K}`]: T[K];
};

type User = { id: string; name: string };
type Renamed = Rename<User>;
// = { new_id: string; new_name: string }

キー除外: as でneverを返すと消える

// 特定キーをneverにマップすると、その項目だけ型から消える
type RemoveKey<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P];
};

type User = { id: string; name: string; password: string };
type SafeUser = RemoveKey<User, "password">;
// = { id: string; name: string }

この「neverにマップしたキーは消える」性質は非常に強力で、PickByValue / OmitByValue / FilterFunctionsなど多くの応用型の土台になります。

プレフィックスを付けてキー変換

// Capitalize でキーの先頭を大文字化
type WithGetters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type User = { id: string; name: string };
type UserGetters = WithGetters<User>;
// = { getId: () => string; getName: () => string }

Getter/Setter自動生成型

// プロパティからgetX/setXを自動生成
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
type Accessors<T> = Getters<T> & Setters<T>;

type User = { id: string; name: string };
type UserAccessors = Accessors<User>;
// = {
//   getId: () => string; getName: () => string;
//   setId: (v: string) => void; setName: (v: string) => void;
// }

クラスや状態オブジェクトのアクセサ型を、定義1行で自動生成できます。Reduxのactions型生成・MobXのstore型・Vuexのcommit型などにも応用が利きます。

実用パターン20選

ここからは現場で「これがあると助かる」型を一気に20パターン紹介します。コピペでそのままプロジェクトに持ち込めるよう、依存ゼロで書いています。

パターン1: EventHandler型の自動生成

// onClick / onChange / onSubmit などのハンドラ型を一括生成
type EventMap = {
  click: { x: number; y: number };
  change: { value: string };
  submit: { form: HTMLFormElement };
};

type EventHandlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]?: (event: T[K]) => void;
};

type Handlers = EventHandlers<EventMap>;
// = {
//   onClick?: (e: { x: number; y: number }) => void;
//   onChange?: (e: { value: string }) => void;
//   onSubmit?: (e: { form: HTMLFormElement }) => void;
// }

パターン2: Redux風Action型生成

// payloadMapからunion型のAction定義を作る
type PayloadMap = {
  ADD_TODO: { text: string };
  REMOVE_TODO: { id: string };
  TOGGLE_TODO: { id: string };
};

type ActionsFromMap<T> = {
  [K in keyof T]: { type: K; payload: T[K] };
}[keyof T];

type Action = ActionsFromMap<PayloadMap>;
// = | { type: "ADD_TODO"; payload: { text: string } }
//   | { type: "REMOVE_TODO"; payload: { id: string } }
//   | { type: "TOGGLE_TODO"; payload: { id: string } }

Mapped Typeの結果に対して[keyof T]を付けると、各値型をユニオンに分解できます。これはMapped Typesでunionを構築するときの定番テクニックです。

パターン3: APIパスからハンドラ型を作る

// HTTPメソッド×パスごとにハンドラ型を要求
type Routes = {
  "GET /users": { response: { id: string; name: string }[] };
  "POST /users": { request: { name: string }; response: { id: string } };
  "DELETE /users/:id": { params: { id: string } };
};

type RouteHandlers = {
  [Path in keyof Routes]: (ctx: Routes[Path]) => Promise<void>;
};

パターン4: Formフィールド型生成

// 各フィールドにvalue/error/touchedを付与
type FormShape = {
  email: string;
  password: string;
  age: number;
};

type FormState<T> = {
  [K in keyof T]: { value: T[K]; error?: string; touched: boolean };
};

type LoginFormState = FormState<FormShape>;
// = {
//   email: { value: string; error?: string; touched: boolean };
//   password: { value: string; error?: string; touched: boolean };
//   age: { value: number; error?: string; touched: boolean };
// }

パターン5: Database スキーマ → 型

// テーブル定義からエンティティ型を生成
type Tables = {
  users: { id: "string"; name: "string"; age: "number" };
  posts: { id: "string"; title: "string"; published: "boolean" };
};

type ColumnType<T> = T extends "string"
  ? string
  : T extends "number"
  ? number
  : T extends "boolean"
  ? boolean
  : never;

type Entity<T> = {
  [K in keyof T]: ColumnType<T[K]>;
};

type User = Entity<Tables["users"]>;
// = { id: string; name: string; age: number }

パターン6: union → object 変換

// ユニオン型のキーから各値を初期化したオブジェクト型を作る
type UnionToObject<U extends string, V> = {
  [K in U]: V;
};

type Status = "loading" | "success" | "error";
type StatusFlags = UnionToObject<Status, boolean>;
// = { loading: boolean; success: boolean; error: boolean }

パターン7: tuple → object 変換

// タプル要素から固定キーのオブジェクト型を作る
type TupleToObject<T extends readonly any[]> = {
  [K in T[number] & string]: K;
};

const colors = ["red", "green", "blue"] as const;
type ColorMap = TupleToObject<typeof colors>;
// = { red: "red"; green: "green"; blue: "blue" }

パターン8: 階層的Mapped Type

// 2階層の型を一括変換
type NestedShape = {
  user: { id: string; name: string };
  post: { id: string; title: string };
};

type ApiResponse<T> = {
  [K in keyof T]: {
    data: T[K];
    fetchedAt: Date;
  };
};

type Resp = ApiResponse<NestedShape>;
// = {
//   user: { data: { id: string; name: string }; fetchedAt: Date };
//   post: { data: { id: string; title: string }; fetchedAt: Date };
// }

パターン9: Distributive Conditional Typesとの組み合わせ

// ユニオン型の各要素を個別に変換
type Wrap<T> = T extends any ? { value: T } : never;

type Wrapped = Wrap<string | number>;
// = { value: string } | { value: number }

ConditionalTypesはユニオンに分配される性質があり、Mapped Typesと組み合わせると複雑なユニオン変換が表現できます。

パターン10: Recursive Mapped Types

// 再帰的にobject型を辿る基本形
type DeepIdentity<T> = {
  [K in keyof T]: T[K] extends object ? DeepIdentity<T[K]> : T[K];
};

type Nested = { a: { b: { c: string } } };
type Same = DeepIdentity<Nested>;
// = { a: { b: { c: string } } }

パターン11: DeepPartial

// ネストされた全プロパティをoptional化
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

type Config = {
  api: { url: string; timeout: number };
  ui: { theme: { primary: string; secondary: string } };
};
type PartialConfig = DeepPartial<Config>;
// 任意の深さでoptionalになる
const cfg: PartialConfig = { ui: { theme: { primary: "#f00" } } }; // OK

設定オブジェクトのマージ・テスト用モック・PATCHリクエストなどで非常によく使う型です。

パターン12: DeepReadonly

// ネストされた全プロパティをreadonly化
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

type State = { user: { id: string; profile: { name: string } } };
type Frozen = DeepReadonly<State>;

const s: Frozen = { user: { id: "1", profile: { name: "A" } } };
// s.user.profile.name = "B"; // ❌ 全階層が書き換え不可

パターン13: DeepRequired

// ネストされた全プロパティを必須化
type DeepRequired<T> = {
  [K in keyof T]-?: T[K] extends object ? DeepRequired<T[K]> : T[K];
};

type DraftConfig = { api?: { url?: string; timeout?: number } };
type StrictConfig = DeepRequired<DraftConfig>;
// = { api: { url: string; timeout: number } }

パターン14: PickByValue

// 値の型でプロパティを絞り込む
type PickByValue<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K];
};

type User = { id: string; name: string; age: number; admin: boolean };
type StringFields = PickByValue<User, string>;
// = { id: string; name: string }

「文字列フィールドだけ抜き出したい」「関数だけ抜き出したい」など、値の型でフィルタしたい場面で重宝します。

パターン15: OmitByValue

// 値の型でプロパティを除外する
type OmitByValue<T, V> = {
  [K in keyof T as T[K] extends V ? never : K]: T[K];
};

type User = { id: string; name: string; age: number; meta: () => void };
type DataOnly = OmitByValue<User, Function>;
// = { id: string; name: string; age: number }

パターン16: NullableAll

// 全プロパティにnullを許可
type NullableAll<T> = {
  [K in keyof T]: T[K] | null;
};

type User = { id: string; name: string };
type DbRow = NullableAll<User>;
// = { id: string | null; name: string | null }

パターン17: NotNullableAll

// 全プロパティからnull/undefinedを除去
type NotNullableAll<T> = {
  [K in keyof T]: NonNullable<T[K]>;
};

type DbRow = { id: string | null; name: string | null };
type Validated = NotNullableAll<DbRow>;
// = { id: string; name: string }

パターン18: 関数だけ抜き出すFunctionProperties

// メソッドだけ抜き出す
type FunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;

class Service {
  url = "";
  get() {}
  post() {}
}
type ServiceMethods = FunctionProperties<Service>;
// = { get: () => void; post: () => void }

パターン19: プロパティを全て関数化(Promise版)

// 各値型をPromise化したAPIクライアントを生成
type ApiContract = {
  fetchUser: { id: string };
  fetchPost: { id: string };
};

type ApiClient<T> = {
  [K in keyof T]: (params: T[K]) => Promise<unknown>;
};

type Client = ApiClient<ApiContract>;
// = {
//   fetchUser: (p: { id: string }) => Promise;
//   fetchPost: (p: { id: string }) => Promise;
// }

パターン20: 列挙型からswitchハンドラ要求型

// 各enum値ごとに必ずハンドラを書かせる(網羅性チェック)
type Status = "loading" | "success" | "error";

type StatusHandlers<R> = {
  [S in Status]: () => R;
};

const handlers: StatusHandlers<string> = {
  loading: () => "Loading...",
  success: () => "Done!",
  error: () => "Failed!",
  // どれか書き忘れると型エラー → 網羅チェックになる
};

ライブラリでのMapped Types実装例

GraphQL CodegenやPrismaのような有名ライブラリは、内部でMapped Typesを多用しています。「スキーマ→TypeScript型」のコア技術がまさにMapped Typesです。

GraphQL Codegen風のレスポンス型生成

// GraphQLレスポンスの全フィールドをnullable化(Codegenの典型変換)
type Maybe<T> = T | null;

type MaybeAll<T> = {
  [K in keyof T]: Maybe<T[K]>;
};

type RawUser = { id: string; name: string; email: string };
type GqlUser = MaybeAll<RawUser>;
// = { id: string | null; name: string | null; email: string | null }

Prisma風のSelect型生成

// 各カラムをbooleanで「取得する/しない」を指定する型
type SelectShape<T> = {
  [K in keyof T]?: boolean;
};

type User = { id: string; name: string; email: string };
type UserSelect = SelectShape<User>;
// = { id?: boolean; name?: boolean; email?: boolean }

const select: UserSelect = { id: true, name: true };

tRPC風のクライアント型生成

// procedure定義から型安全なクライアントを構築
type Procedures = {
  getUser: { input: { id: string }; output: { name: string } };
  listPosts: { input: void; output: { id: string }[] };
};

type Client<T extends Record<string, { input: any; output: any }>> = {
  [K in keyof T]: (input: T[K]["input"]) => Promise<T[K]["output"]>;
};

type AppClient = Client<Procedures>;
// = {
//   getUser: (i: { id: string }) => Promise;
//   listPosts: (i: void) => Promise;
// }

パフォーマンスと落とし穴

Mapped Typesは強力ですが、無制限に使うとIDE体感が遅くなったり、エラーメッセージが読めなくなったりします。実戦で知っておきたい注意点をまとめます。

落とし穴1: 再帰の深さ制限

// 再帰が深すぎると "Type instantiation is excessively deep" エラー
type InfiniteDeep<T> = {
  [K in keyof T]: T[K] extends object ? InfiniteDeep<T[K]> : T[K];
};

// 50階層級のネストや循環参照を持つ型で爆発する

TypeScriptには再帰深度の上限があり、深すぎる型はコンパイルエラーやIDEの応答低下を引き起こします。深いネストにはanyunknownでガードする実装が現実解です。

落とし穴2: 巨大ユニオンに対するMapped Types

// 数百のキーを持つMapped Typesは型チェックが重い
type HugeUnion = "a" | "b" | "c" | /* ...500個... */ "zzz";
type HeavyMap = { [K in HugeUnion]: number };

// 可能ならRecordを使う(同じ意味だがTSが最適化しやすい)

落とし穴3: as nevernで消したつもりが残る

// ❌ neverを返さず undefined を返すと、キーは消えない
type Wrong<T, K extends keyof T> = {
  [P in keyof T as P extends K ? undefined : P]: T[P];
};

// ✅ きちんと never にする
type Correct<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P];
};

落とし穴4: 推論を阻害するMapped Types

// 関数引数にMapped Typesを使うと推論が止まることがある
declare function applyMap<T>(t: { [K in keyof T]: T[K] }): T;

// この場合は素直にTを受け取るほうが推論が効く
declare function applyDirect<T>(t: T): T;

Mapped Typesは型変換に強い一方、引数型の入口で使うとTがうまく推論されない場合があります。「変換は出口で」「入口は素直に」が経験則として有効です。

Before / After リファクタリング集

最後に、現場でよくある「型を手書きしすぎている」コードをMapped Typesで一掃するリファクタ例をいくつか紹介します。

Before/After 1: APIレスポンスの全フィールドnullable

// ❌ Before: 手書き重複
type UserApi = {
  id: string | null;
  name: string | null;
  email: string | null;
  avatar: string | null;
};
// ✅ After: Mapped Typesで派生
type UserCore = { id: string; name: string; email: string; avatar: string };
type UserApi = { [K in keyof UserCore]: UserCore[K] | null };

Before/After 2: Form状態

// ❌ Before: 同じ構造をフィールド数分書く
type LoginForm = {
  email: { value: string; error?: string };
  password: { value: string; error?: string };
};
// ✅ After: Mapped Typesで包む
type LoginShape = { email: string; password: string };
type LoginForm = {
  [K in keyof LoginShape]: { value: LoginShape[K]; error?: string };
};

Before/After 3: Reduxアクション

// ❌ Before: アクション増加のたびに手書き
type Action =
  | { type: "ADD_TODO"; payload: { text: string } }
  | { type: "REMOVE_TODO"; payload: { id: string } }
  | { type: "TOGGLE_TODO"; payload: { id: string } };
// ✅ After: PayloadMapから自動生成
type PayloadMap = {
  ADD_TODO: { text: string };
  REMOVE_TODO: { id: string };
  TOGGLE_TODO: { id: string };
};
type Action = { [K in keyof PayloadMap]: { type: K; payload: PayloadMap[K] } }[keyof PayloadMap];

Before/After 4: Getter群

// ❌ Before: getter全部手書き
type UserGetters = {
  getId: () => string;
  getName: () => string;
  getEmail: () => string;
};
// ✅ After: Capitalize + Mapped Types
type User = { id: string; name: string; email: string };
type UserGetters = {
  [K in keyof User as `get${Capitalize<string & K>}`]: () => User[K];
};

Before/After 5: PATCH用のPartial DTO

// ❌ Before: ネストごとにoptionalを手書き
type PatchConfig = {
  api?: { url?: string; timeout?: number };
  ui?: { theme?: { primary?: string } };
};
// ✅ After: DeepPartialで一発
type Config = {
  api: { url: string; timeout: number };
  ui: { theme: { primary: string } };
};
type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] };
type PatchConfig = DeepPartial<Config>;

収益化のための学習ロードマップ

Mapped Typesまで自在に扱えるようになると、フロントエンドの「型設計ができる人」として実務市場での評価がぐっと高まります。React+TypeScriptの中〜大規模案件では、型を整理してチーム全体の生産性を上げる役割は単価が伸びやすい領域です。

独学で詰まってきたら、体系的なカリキュラム+メンター付きのスクールで一気にレベルアップする選択肢も現実的です。以下のような選択肢を比較しておくとよいでしょう。

  • テックアカデミー: TypeScript・React・Node.jsの実務直結カリキュラム。週2回のメンタリングで現役エンジニアに型設計の相談ができる
  • 侍エンジニア: マンツーマンで自分のポートフォリオをレビューしてもらえる。Mapped Typesを使った型基盤の設計力を実案件レベルに引き上げやすい
  • DMM WEBCAMP: 転職保証付きコースあり。学習中の挫折率を下げたい人に向く
  • レバテックキャリア: TypeScript経験者の転職に強いエージェント。型設計の経験はポートフォリオに書くと刺さりやすい

まとめ: Mapped Typesは「型のfor文」

Mapped Typesは「型のfor文」のような存在で、既存の型から派生型を量産する強力な仕組みです。本記事のポイントを振り返ります。

  • 基本構文は[K in keyof T]: T[K]。これを変形してUtility Typesを自作できる
  • + / -modifierでreadonly?を自在に付け外しできる
  • Key Remapping(as)キー名そのものを変換でき、Getter/Setter自動生成などが可能
  • Conditional Typesとの組み合わせでDeepPartial・PickByValueなどの実用型が書ける
  • 再帰深度・推論阻害・never消去の挙動など落とし穴も把握しておく

Utility Typesを「使う側」から「作る側」に踏み出せたら、TypeScriptの型表現力は別次元に到達します。日々のコードベースで「同じ型を2回書いている」場面を見つけたら、まずMapped Typesでまとめられないかを疑ってみてください。それだけでコード量と保守コストの両方が確実に下がります。

コメント

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