【TypeScript】Mapped Types(マップ型)完全ガイド|[K in keyof T]・モディファイア・キーリマッピング・実務パターン徹底解説

【TypeScript】Mapped Types(マップ型)完全ガイド|[K in keyof T]・モディファイア・キーリマッピング・実務パターン徹底解説 TypeScript

Mapped Types(マップ型)は、既存の型をベースに全プロパティを一括変換して新しい型を作る機能です。[K in keyof T]: T[K] という構文で「T の各プロパティ K に対して…」と処理を定義します。

Partial<T>Required<T>Readonly<T>Record<K,V> など、TypeScript の標準ユーティリティ型の多くが Mapped Types で実装されています。本記事では基本構文からモディファイア・キーリマッピング(as 句)・条件型との組み合わせまで、実践パターンを含めて完全解説します。

この記事でわかること

  • Mapped Types の基本構文 [K in keyof T]: T[K] の仕組み
  • readonly? モディファイアと +/- 修飾子で属性を追加・除去する方法
  • キーリマッピング(as 句)でプロパティ名を変換・フィルタする方法
  • Homomorphic と Non-Homomorphic の違いと使い分け
  • 条件型・テンプレートリテラル型との組み合わせパターン
  • 標準ユーティリティ型の実装解読・実践パターン3本・FAQ6問
スポンサーリンク

1. Mapped Types の基本構文

Mapped Types は { [K in Union]: ValueType } という形式で書きます。K in Union は「Union 型の各メンバー K に対して」という意味で、オブジェクト型の各プロパティに対して処理を定義できます。

// 最もシンプルな Mapped Type:プロパティをそのままコピー
type Copy<T> = {
    [K in keyof T]: T[K];
};

interface User {
    id: number;
    name: string;
    email: string;
    age: number;
}

type UserCopy = Copy<User>;
// { id: number; name: string; email: string; age: number }

// 全プロパティを boolean に変換
type Flags<T> = {
    [K in keyof T]: boolean;
};

type UserFlags = Flags<User>;
// { id: boolean; name: boolean; email: boolean; age: boolean }

// Union 型から固定値のオブジェクト型を作る(Record の実装)
type MyRecord<K extends string, V> = {
    [P in K]: V;
};

type StringConfig = MyRecord<"host" | "port" | "db", string>;
// { host: string; port: string; db: string }
keyof T vs Union 型:どちらも使える
[K in keyof T] は「T の全プロパティキーを反復」します。[K in "a" | "b" | "c"] のように直接 Union 型を書くこともできます。前者は元の型のプロパティを保持するHomomorphicな変換で、後者は新規作成のNon-Homomorphicな変換です(後述)。

2. モディファイア:readonly と ? の追加・除去

Mapped Types ではプロパティに readonly(読み取り専用)と ?(省略可能)を追加・除去できます。+ で追加(デフォルト)、- で除去します。

構文 意味 代表的なユーティリティ型
[K in keyof T]?: 全プロパティを省略可能にする Partial<T>
[K in keyof T]-?: 全プロパティを必須にする(? を除去) Required<T>
readonly [K in keyof T]: 全プロパティを読み取り専用にする Readonly<T>
-readonly [K in keyof T]: readonly を除去して変更可能にする -readonly カスタム型
// Partial の実装(全プロパティを省略可能に)
type MyPartial<T> = {
    [K in keyof T]?: T[K];
};

// Required の実装(全プロパティを必須に)
type MyRequired<T> = {
    [K in keyof T]-?: T[K];
};

// Readonly の実装(全プロパティを readonly に)
type MyReadonly<T> = {
    readonly [K in keyof T]: T[K];
};

// Mutable:readonly を全て除去する(標準にない、よく使うカスタム型)
type Mutable<T> = {
    -readonly [K in keyof T]: T[K];
};

type ReadonlyUser = MyReadonly<User>;
// { readonly id: number; readonly name: string; ... }

type MutableUser = Mutable<ReadonlyUser>;
// { id: number; name: string; ... }(readonly が全て除去)

// 組み合わせ:readonly かつ省略可能
type ReadonlyPartial<T> = {
    readonly [K in keyof T]?: T[K];
};
-? と Required の注意点
-?undefined を型から除去しません。{ age?: number }Required を適用すると{ age: number | undefined } ではなく { age: number } になりますが、これは strict モードの場合のみです。strictNullChecks: false の環境では挙動が変わります。NonNullable と組み合わせてプロパティ値から undefined も除くには[K in keyof T]-?: NonNullable<T[K]> と書きます。

3. キーリマッピング(as 句)

TypeScript 4.1 から、Mapped Types の as 句を使ってプロパティ名を変換・フィルタできるようになりました。テンプレートリテラル型と組み合わせると非常に強力です。

3-1. キーのリネーム(テンプレートリテラル型との組み合わせ)

// getter メソッド名を自動生成
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<User>;
// {
//   getId: () => number;
//   getName: () => string;
//   getEmail: () => string;
//   getAge: () => number;
// }

// setter メソッド名を自動生成
type Setters<T> = {
    [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

// on〇〇Change イベントハンドラを自動生成
type ChangeHandlers<T> = {
    [K in keyof T as `on${Capitalize<string & K>}Change`]?: (value: T[K]) => void;
};

type UserChangeHandlers = ChangeHandlers<User>;
// {
//   onIdChange?: (value: number) => void;
//   onNameChange?: (value: string) => void;
//   ...
// }

3-2. キーのフィルタリング(never で除外)

as 句で never を返すとそのキーは除外されます。条件型と組み合わせることで、特定の型のプロパティだけを残すことができます。

// 関数型プロパティのみを残す
type MethodsOnly<T> = {
    [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
};

interface Api {
    baseUrl: string;
    timeout: number;
    getUser(id: number): Promise<User>;
    updateUser(id: number, data: Partial<User>): Promise<void>;
    deleteUser(id: number): Promise<boolean>;
}

type ApiMethods = MethodsOnly<Api>;
// { getUser: ...; updateUser: ...; deleteUser: ... }

// 特定の型のプロパティのキーのみ抽出(PickByValue)
type PickByValue<T, V> = {
    [K in keyof T as T[K] extends V ? K : never]: T[K];
};

interface Form {
    username: string;
    email:    string;
    age:      number;
    agreed:   boolean;
    score:    number;
}

type StringFields  = PickByValue<Form, string>;  // { username: string; email: string }
type NumberFields  = PickByValue<Form, number>;  // { age: number; score: number }
type BooleanFields = PickByValue<Form, boolean>; // { agreed: boolean }

4. Homomorphic と Non-Homomorphic の違い

Mapped Types にはHomomorphic(準同型)Non-Homomorphic(非準同型)の2種類があります。この違いを理解すると、readonly? が保持されるかどうかが予測できます。

種類 構文の特徴 元の修飾子 代表例
Homomorphic [K in keyof T](元の型の keyof を使う) 保持される PartialReadonlyRequired
Non-Homomorphic [K in SomeUnion](外部の Union を使う) 保持されない Record・独自の Union マッピング
interface Config {
    readonly host: string;
    port?: number;
    debug: boolean;
}

// Homomorphic: keyof T を使う → 元の修飾子を保持
type HomoMapped<T> = { [K in keyof T]: T[K] };
type H = HomoMapped<Config>;
// { readonly host: string; port?: number; debug: boolean }
// ↑ readonly と ? がそのまま保持される

// Non-Homomorphic: 外部 Union を使う → 修飾子は保持されない
type NonHomoMapped<T, K extends keyof T> = { [P in K]: T[P] };
type NH = NonHomoMapped<Config, keyof Config>;
// { host: string; port: number; debug: boolean }
// ↑ readonly と ? が失われる

// Record は Non-Homomorphic(固定の Union を使うため修飾子なし)
type R = Record<keyof Config, string>;
// { host: string; port: string; debug: string }(readonly/? なし)
Homomorphic の方が「安全」な変換
[K in keyof T] を使う Homomorphic な Mapped Types は元の型の readonly? を自動的に引き継ぎます。意図的に除去したい場合は -readonly-? を明示します。Non-Homomorphic は常に素のプロパティになるため、修飾子を気にしない場面で使います。

5. 標準ユーティリティ型の実装解読

TypeScript の標準ユーティリティ型を Mapped Types の観点から読み解きます。実装を理解することで、カスタムユーティリティ型の作成が自然にできるようになります。

// Partial<T>: 全プロパティを省略可能に
type Partial<T> = { [P in keyof T]?: T[P]; };

// Required<T>: 全プロパティを必須に(? を除去)
type Required<T> = { [P in keyof T]-?: T[P]; };

// Readonly<T>: 全プロパティを readonly に
type Readonly<T> = { readonly [P in keyof T]: T[P]; };

// Record<K, T>: キー K・値 T のオブジェクト型(Non-Homomorphic)
type Record<K extends keyof any, T> = { [P in K]: T; };

// Pick<T, K>: T から指定キー K のみを持つ型を作る
type Pick<T, K extends keyof T> = { [P in K]: T[P]; };

// Omit<T, K>: T から指定キー K を除いた型を作る
// TypeScript の内部実装は Exclude<keyof T, K> を Mapped Types に渡す
type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P]; };

// 使用例
interface Post { id: number; title: string; body: string; authorId: number; }
type PostPreview  = Pick<Post, "id" | "title">;        // { id: number; title: string }
type PostWithoutId = Omit<Post, "id">;                  // { title: string; body: string; authorId: number }
type PartialPost  = Partial<Pick<Post, "title"|"body">>; // { title?: string; body?: string }

ユーティリティ型の使い方の詳細は ユーティリティ型完全ガイド を参照してください。

6. 条件型・テンプレートリテラル型との組み合わせ

6-1. 条件型と組み合わせた動的な値型変換

// 各プロパティの型を「その型 | null」に変換(Nullable)
type Nullable<T> = {
    [K in keyof T]: T[K] | null;
};

// 各プロパティを Promise でラップ
type Promisify<T> = {
    [K in keyof T]: T[K] extends (...args: infer A) => infer R
        ? (...args: A) => Promise<R>
        : Promise<T[K]>;
};

interface SyncService {
    getData(): string[];
    getUser(id: number): User;
    name: string;
}

type AsyncService = Promisify<SyncService>;
// {
//   getData: () => Promise<string[]>;
//   getUser: (id: number) => Promise<User>;
//   name: Promise<string>;
// }

// 深い型変換:ネストしたオブジェクトも再帰的に変換
type DeepReadonly<T> = {
    readonly [K in keyof T]: T[K] extends object
        ? T[K] extends (...args: any[]) => any
            ? T[K]          // 関数はそのまま
            : DeepReadonly<T[K]>  // オブジェクトは再帰
        : T[K];             // プリミティブはそのまま
};

6-2. テンプレートリテラル型との組み合わせ

// CSS モジュールのクラス名型を自動生成
type CSSModule<T extends string> = {
    [K in T]: string;
};

type Styles = CSSModule<"container" | "header" | "main" | "footer">;
// { container: string; header: string; main: string; footer: string }

// プロパティ名を snake_case から camelCase に変換する型
type SnakeToCamel<S extends string> =
    S extends `${infer Head}_${infer Tail}`
        ? `${Head}${Capitalize<SnakeToCamel<Tail>>}`
        : S;

type CamelKeys<T> = {
    [K in keyof T as SnakeToCamel<string & K>]: T[K];
};

interface ApiResponse {
    user_id: number;
    first_name: string;
    last_name: string;
    created_at: string;
}

type CamelApiResponse = CamelKeys<ApiResponse>;
// {
//   userId: number;
//   firstName: string;
//   lastName: string;
//   createdAt: string;
// }

テンプレートリテラル型との組み合わせの詳細は テンプレートリテラル型完全ガイド を参照してください。

7. 実践パターン3本

実践例1:API レスポンスの型を自動変換するアダプター

バックエンドから snake_case で返ってくる API レスポンスを、フロントエンドの camelCase に変換する型を Mapped Types で自動生成するパターンです。

// snake_case → camelCase の型変換(再帰対応)
type SnakeToCamel<S extends string> =
    S extends `${infer Head}_${infer Tail}`
        ? `${Head}${Capitalize<SnakeToCamel<Tail>>}`
        : S;

type KeysToCamel<T> = T extends object
    ? T extends any[]
        ? KeysToCamel<T[number]>[]
        : { [K in keyof T as SnakeToCamel<string & K>]: KeysToCamel<T[K]> }
    : T;

// バックエンドの型定義
interface SnakeOrder {
    order_id: number;
    user_id: number;
    created_at: string;
    order_items: Array<{
        product_id: number;
        unit_price: number;
        quantity: number;
    }>;
}

// 自動変換後のフロントエンド型
type CamelOrder = KeysToCamel<SnakeOrder>;
// {
//   orderId: number;
//   userId: number;
//   createdAt: string;
//   orderItems: Array<{
//     productId: number;
//     unitPrice: number;
//     quantity: number;
//   }>;
// }

// 変換関数にも同じ型を使う
function toCamel<T>(data: T): KeysToCamel<T> {
    if (Array.isArray(data)) return data.map(toCamel) as any;
    if (typeof data !== "object" || data === null) return data as any;
    return Object.fromEntries(
        Object.entries(data as any).map(([k, v]) => [
            k.replace(/_([a-z])/g, (_match: string, c: string) => c.toUpperCase()),
            toCamel(v),
        ])
    ) as any;
}

実践例2:バリデーションスキーマの型安全な定義

フォームの各フィールドに対するバリデーションルールを Mapped Types で型安全に定義し、フィールドの追加・削除が型に自動反映されるパターンです。

// バリデーションルールの型(フィールドの型ごとにバリデーター関数の型が変わる)
type FieldValidator<T> =
    T extends string  ? { minLength?: number; maxLength?: number; pattern?: RegExp; required?: boolean } :
    T extends number  ? { min?: number; max?: number; required?: boolean } :
    T extends boolean ? { required?: boolean } :
    { required?: boolean };

// フォーム型から自動的にバリデーションスキーマ型を生成
type ValidationSchema<T> = {
    [K in keyof T]: FieldValidator<T[K]> & {
        label?: string;
        errorMessages?: Partial<Record<keyof FieldValidator<T[K]>, string>>;
    };
};

interface SignupForm {
    username: string;
    email:    string;
    age:      number;
    agreed:   boolean;
}

const schema: ValidationSchema<SignupForm> = {
    username: {
        label: "ユーザー名",
        minLength: 3,
        maxLength: 20,
        pattern: /^[a-zA-Z0-9_]+$/,
        required: true,
        errorMessages: { minLength: "3文字以上入力してください" },
    },
    email: { label: "メールアドレス", required: true, pattern: /^[^@]+@[^@]+$/ },
    age:   { label: "年齢", min: 18, max: 120, required: true },
    agreed: { label: "利用規約への同意", required: true },
    // SignupForm にないフィールドを追加するとコンパイルエラー
};

実践例3:型安全なストアの実装(getter・action の型自動生成)

状態管理ストアの state 定義から、getter・action・computed の型を Mapped Types で自動生成するパターンです。

// State から getter 型を生成(各プロパティを関数でラップ)
type Getters<State> = {
    [K in keyof State as `get${Capitalize<string & K>}`]: () => State[K];
};

// State から setter 型を生成
type Setters<State> = {
    [K in keyof State as `set${Capitalize<string & K>}`]: (value: State[K]) => void;
};

// State からリセット関数型を生成
type Resetters<State> = {
    [K in keyof State as `reset${Capitalize<string & K>}`]: () => void;
};

// 完全なストア型
type Store<State> = State & Getters<State> & Setters<State> & Resetters<State> & {
    reset(): void;
    subscribe(listener: (state: State) => void): () => void;
};

interface AppState {
    user:    { id: number; name: string } | null;
    loading: boolean;
    theme:   "light" | "dark";
}

type AppStore = Store<AppState>;
// AppState のプロパティ + getUser/getLoading/getTheme
//                       + setUser/setLoading/setTheme
//                       + resetUser/resetLoading/resetTheme
//                       + reset() + subscribe()

// AppState のプロパティを追加するだけで Store の型が自動的に更新される

キーリマッピングとテンプレートリテラル型の詳細は keyof・typeof・インデックスアクセス型ガイド を参照してください。

8. まとめ:Mapped Types 使い分けチートシート

やりたいこと 構文
全プロパティをコピー [K in keyof T]: T[K] カスタム型のベース・型の複製
省略可能にする [K in keyof T]?: T[K] Partial<T>
必須にする [K in keyof T]-?: T[K] Required<T>
readonly を付ける readonly [K in keyof T]: T[K] Readonly<T>
readonly を外す -readonly [K in keyof T]: T[K] Mutable<T>
キー名を変換 [K in keyof T as `prefix${Capitalize<...>}`] getter/setter 名生成・camelCase 変換
プロパティをフィルタ [K in keyof T as Cond ? K : never] PickByValue・メソッドのみ抽出
値の型を変換 [K in keyof T]: NewType<T[K]> NullablePromisify

Mapped Types は TypeScript の型システムで最も表現力の高い機能の一つです。標準の PartialRequiredReadonly の実装を手元で書いてみることから始め、次に as 句でのキーリマッピング、条件型との組み合わせと段階的に習得していきましょう。 条件型との組み合わせは 条件型完全ガイド、Mapped Types を含む高度な型の全体像は 高度な型完全ガイド も参照してください。

FAQ

QMapped Types と interface の extends はどう使い分けますか?

Ainterface extends は型のプロパティを継承・追加するための構文で、静的な型定義に向いています。Mapped Types は既存の型を動的に変換するためのもので、全プロパティに一括処理を適用したい場合に使います。「すべてを省略可能にしたい」「すべてを読み取り専用にしたい」など、変換が必要な場面では Mapped Types を選びましょう。

Q-? と Required の違いは?標準の Required を使えばよいですか?

ARequired<T> は標準の実装が [P in keyof T]-?: T[P] なので全く同じです。自分で [K in keyof T]-?: T[K] と書くのと Required<T> を使うのは同じ結果になります。可読性のために標準ユーティリティ型が存在する場合はそちらを優先しましょう。カスタムの変換(値型も変えたい場合など)では自分で Mapped Types を書く必要があります。

Q[K in keyof T] と [P in keyof T] では違いがありますか?

Aありません。KPKey などはすべて型変数名で、名前の違いだけです。慣習として K(Key の略)や P(Property の略)がよく使われます。関連する型変数との整合性を意識して名前を選んでください。

Qキーリマッピング(as 句)は TypeScript のどのバージョンから使えますか?

ATypeScript 4.1(2020年11月リリース)から使えます。それ以前のバージョンでは [K in keyof T as ...] の構文はコンパイルエラーになります。tsconfig.json の TypeScript バージョンを確認してください。テンプレートリテラル型との組み合わせも同じく 4.1 以降です。

QMapped Types で配列型はどう扱われますか?

AT = string[]Partial<T> を適用すると (string | undefined)[] ではなくstring[] | undefined になることはなく、実際には配列型を keyof で反復するとlengthpush など配列のメソッドも含まれます。そのため配列型に直接 Mapped Types を適用するのは通常避け、T extends any[] ? ... : Mapped<T> のように条件型で場合分けするのが一般的です。

QMapped Types と Conditional Types を同時に使うと型チェックが遅くなりますか?

A複雑な Mapped Types(特に再帰的なもの)は型チェックのパフォーマンスに影響することがあります。対策として①再帰の深さを制限する②よく使う型は type エイリアスにキャッシュする(同じ型計算を何度もさせない)③interface で宣言マージを使う(TypeScript がキャッシュしやすい)などがあります。tsc --diagnostics で遅い箇所を特定できます。

Mapped Types は最初は難しく感じますが、PartialReadonly の実装を手元で再現することから始めると自然に理解が深まります。 条件型完全ガイド も合わせて読むと、組み合わせパターンがより理解できます。