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型生成などの実戦応用
- Mapped Typesとは何か
- 基本のマッピング
- 標準Utility Typesを自作する
- + / – modifierを使いこなす
- Key Remapping (as 句)
- 実用パターン20選
- パターン1: EventHandler型の自動生成
- パターン2: Redux風Action型生成
- パターン3: APIパスからハンドラ型を作る
- パターン4: Formフィールド型生成
- パターン5: Database スキーマ → 型
- パターン6: union → object 変換
- パターン7: tuple → object 変換
- パターン8: 階層的Mapped Type
- パターン9: Distributive Conditional Typesとの組み合わせ
- パターン10: Recursive Mapped Types
- パターン11: DeepPartial
- パターン12: DeepReadonly
- パターン13: DeepRequired
- パターン14: PickByValue
- パターン15: OmitByValue
- パターン16: NullableAll
- パターン17: NotNullableAll
- パターン18: 関数だけ抜き出すFunctionProperties
- パターン19: プロパティを全て関数化(Promise版)
- パターン20: 列挙型からswitchハンドラ要求型
- ライブラリでのMapped Types実装例
- パフォーマンスと落とし穴
- Before / After リファクタリング集
- 収益化のための学習ロードマップ
- まとめ: 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を自作する
ここからはPartialやPickなどの標準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の応答低下を引き起こします。深いネストにはanyかunknownでガードする実装が現実解です。
落とし穴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でまとめられないかを疑ってみてください。それだけでコード量と保守コストの両方が確実に下がります。

コメント