🔰【初心者向け】TypeScriptのMapped Types入門ガイド！型変換をもっと自由に＆安全に✨

# TypeScriptのMapped Types完全ガイド：型の変換を自在に操る技術

私はプログラミング歴1年半になるエンジニアで、最近はExcel VBAの連載記事を執筆していましたが、今回はフロントエンド開発で重要になるTypeScriptの高度な型システムについて解説します。特に「Mapped Types（マップドタイプ）」という強力な機能について、その仕組みと実用的な活用方法を詳しく説明していきます。

Mapped Typesは、既存の型から新しい型を生成する際に使用される高度な型操作技術で、TypeScriptの型安全性を最大限活用するために欠かせない知識です。一見複雑に見えますが、その仕組みを理解することで、より柔軟で保守性の高いコードを書くことができるようになります。

## 目次

- [はじめに](#はじめに)
- [Mapped Typesとは](#mapped-typesとは)
- [基本的な構文と仕組み](#基本的な構文と仕組み)
- [キーの操作技術](#キーの操作技術)
- [条件付き型との組み合わせ](#条件付き型との組み合わせ)
- [実用的な応用例](#実用的な応用例)
- [組み込みユーティリティ型の仕組み](#組み込みユーティリティ型の仕組み)
- [パフォーマンスと制約事項](#パフォーマンスと制約事項)
- [実務でのベストプラクティス](#実務でのベストプラクティス)
- [まとめ](#まとめ)

## はじめに

TypeScriptは、JavaScriptを基盤にした型付けをサポートする言語です。型システムを徹底的に活用することで、開発者はバグを減らし、コードの保守性を高めることが可能です。しかし、TypeScriptを深く理解するには、特に「Mapped Types（マップドタイプ）」のような高度な機能を正しく操作するスキルが求められます。この機能により、既存の型を基に新たな型を生成し、型安全性を維持しつつ柔軟なコードを書くことができます。

実務では、APIから取得したデータをユーザーインターフェース用に変換したり、データベースエンティティ（データベースに保存されている実体[例: テーブルの1行を指すレコードなど、データベースに保存されている個々のデータ]）の一部だけを公開用に加工する必要が生じます。これらの課題に対して、「Mapped Types」を活用することで、重複コードの削減やメンテナンス性の向上が図れます。

本記事では、TypeScriptの「Mapped Types」という強力な型操作機能に焦点を当て、これらの課題を効率的に解決する方法を解説します。

## Mapped Typesとは

### 基本概念

Mapped Types（マップ型）は、既存の型のプロパティを反復処理して、新しい型を生成するTypeScriptの機能です。JavaScriptの配列メソッド `map()` と似た概念で、「型に対するmap操作」と考えることができます。

:::note info
### 型のマッピングとは

プログラミングにおける「マッピング」とは、あるデータ構造の各要素に対して特定の処理を適用し、新しいデータ構造を生成することです。Mapped Typesでは、型のプロパティが「要素」にあたり、各プロパティに対して型変換処理を適用して新しい型を生成します。
:::

### なぜMapped Typesが必要なのか

従来の型定義では、似たような構造の型を複数定義する必要がありました。例えば、ユーザー情報を表す型と、そのユーザー情報の更新用の型（すべてのプロパティがオプショナル）を別々に定義していました。
※ オプショナルについては、後述します。

```typescript
// ユーザー情報を表す型
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// 手動で全プロパティをオプショナルに定義
interface UserUpdate {
  id?: number;
  name?: string;
  email?: string;
  age?: number;
}
```

この方法には以下の問題があります。

1. **コードの重複**: 似たような型定義を繰り返し書く必要がある
2. **保守性の低下**: 元の型が変更されても、関連する型は自動的に更新されない
3. **人的ミスの可能性**: 手動で型を定義する際に、プロパティの漏れや型の間違いが発生しやすい

Mapped Typesを使用することで、これらの問題を効率的に解決できます。

## 基本的な構文と仕組み

### 基本構文の解析

Mapped Typesの基本構文は以下の形式です。

```typescript
type MappedType<T> = {
  [K in keyof T]: T[K]
}
```

この構文を詳しく分解して説明します。

#### `[K in keyof T]` の解析

この部分は「インデックス署名」と呼ばれ、以下の要素で構成されています。

- **`K`**: 型変数（プロパティキーを表す）
- **`in`**: イテレーション演算子（「〜の中の各要素に対して」という意味）
- **`keyof T`**: 型 `T` のすべてのプロパティキーの Union型(「AまたはBまたはC」のように複数の値のうちいずれか一つを表す型)

#### 動作の仕組み

1. `keyof T` で型 `T` のすべてのプロパティキーを取得
2. `in` 演算子でそれぞれのキーに対して反復処理を実行
3. 各キー `K` に対して `: T[K]` で対応する型を指定
4. 結果として新しい型が生成される

### 具体的な動作例

以下の例で、Mapped Typesがどのように動作するかを見てみましょう。

```typescript
type Person = {
  name: string;
  age: number;
  isActive: boolean;
}
// すべてのプロパティをオプショナルにするMapped Type
type PartialPerson = {
  [K in keyof Person]?: Person[K]
}
```

この処理は以下のステップで実行されます。

1. `keyof Person` → `"name" | "age" | "isActive"`
2. 各キーに対して反復処理：
   - `K = "name"` のとき → `name?: string`
   - `K = "age"` のとき → `age?: number`
   - `K = "isActive"` のとき → `isActive?: boolean`
3. 最終的な型
   ```typescript
   type PartialPerson = {
     name?: string;
     age?: number;
     isActive?: boolean;
   }
   ```

:::note info
### オプショナルプロパティとは

オプショナルプロパティとは、オブジェクトの型定義において「あってもなくても良い」プロパティのことです。プロパティ名の後に `?` を付けることで定義され、そのプロパティを持たないオブジェクトでも型エラーになりません。例えば `PartialPerson` 型では、`{}` (空のオブジェクト) や `{ name: "太郎" }` (一部のプロパティのみ) といったオブジェクトも有効になります。
:::

## キーの操作技術

Mapped Typesでは、プロパティのキー自体も変換することができます。これにより、より柔軟な型変換が可能になります。

### キーの修飾

#### オプショナル修飾子

```typescript
// すべてのプロパティをオプショナルにする
type Optional<T> = {
  [K in keyof T]?: T[K]
}

// すべてのプロパティを必須にする（オプショナル修飾子を削除）
type Required<T> = {
  [K in keyof T]-?: T[K]
}
```

#### readonly修飾子

```typescript
// すべてのプロパティを読み取り専用にする
type Readonly<T> = {
  readonly [K in keyof T]: T[K]
}

// 読み取り専用修飾子を削除する
type Mutable<T> = {
  -readonly [K in keyof T]: T[K]
}
```

:::note info
### 修飾子の操作

- `?` : プロパティをオプショナルにする
- `-?` : オプショナル修飾子を削除する（必須にする）
- `readonly` : プロパティを読み取り専用にする
- `-readonly` : 読み取り専用修飾子を削除する
:::

### キー名の変換

マッピング型内で `as` キーワードを使うと、キーの名前を変換できます。
例えば、プロパティ名にプレフィックスを追加したり、特定の命名規則に変換したりする場合に便利です。Template Literal Types（`${...}`構文）と組み合わせることで、型レベルでの文字列操作が可能になります。

:::note info
#### Template Literal Types（テンプレートリテラル型）
TypeScript 4.1で導入された機能で、JavaScriptのテンプレートリテラル（バッククォートで囲まれた文字列）と同様の構文を型の世界で使用できます。`${Type1}${Type2}`のように型を組み合わせて新しい文字列型を作成できます。これにより、型レベルでの文字列操作や文字列の結合が可能になり、より表現力豊かな型定義が実現できます。
:::

```typescript
// キー名にプレフィックスを追加
type AddPrefix<T, Prefix extends string> = {
  [K in keyof T as `${Prefix}${string & K}`]: T[K]
}

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

// すべてのキーに "user_" プレフィックスを追加
type UserWithPrefix = AddPrefix<User, "user_">
// 結果: { user_name: string; user_age: number; }
```

:::note info
#### extends の基本
TypeScriptの `extends` キーワードは「〜の一種である」や「〜の条件を満たす」という制約を表します。`K extends keyof T` という記述は、「型パラメータKはT型のプロパティ名（キー）の中から選ばれなければならない」という意味です。
この制約により、存在しないプロパティを誤って指定することを防ぎ、型安全性を高めています。
:::


### キーのフィルタリング

マッピング型と `conditional types` （条件付き型）を組み合わせることで、特定の条件に基づいてオブジェクトのプロパティをフィルタリングできます。`as`節で条件式を使用し、条件に合致しないキーを`never`型にすることでそのプロパティを除外したり、特定のキーのみを抽出したりできます。これはオブジェクト型から必要な部分だけを取り出す際に非常に便利な手法です。

```typescript
type User = {
  id: number;
  name: string;
  email: string;
  password: string;
}

// 特定のキーを除外する
type Omit<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P]
}

// id と password を除外した型を作成
type PublicUser = Omit<User, 'id' | 'password'>
// 結果: { name: string; email: string; }

// 特定のキーのみを抽出する
type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

// name と email のみを含む型を作成
type UserBasicInfo = Pick<User, 'name' | 'email'>
// 結果: { name: string; email: string; }
```

:::note info
#### Conditional Types（条件付き型）
TypeScriptの機能で、`T extends U ? X : Y`という三項演算子のような構文を使って、条件に基づいて型を選択できます。マッピング型の`as`節と組み合わせると、条件に基づいてキー名をフィルタリングできます。たとえば、`P extends K ? never : P`という式では、`P`が`K`に含まれる場合は`never`（そのキーを除外）、そうでなければ元のキー名`P`を使うという意味になります。
:::
  


## 条件付き型との組み合わせ

前述のマッピング型と `conditional types` （条件付き型）の組み合わせで、型の構造を保ちながら特定の条件に基づいてプロパティを変換・フィルタリングできます。

### 基本的な条件付きマッピング

```typescript
type User = {
  name: string;
  age: number;
  email: string;
}

// 文字列型のプロパティのみを抽出
type StringPropsOnly<T> = {
  [K in keyof T]: T[K] extends string ? T[K] : never
}

type StringOnlyUser = StringPropsOnly<User>;
// 結果: { name: string; age: never; email: string; }
// 注意: neverはマップされますが、実際のオブジェクト型としては除外されません
```
この例では`T[K] extends string ? T[K] : never`という条件式で、「そのプロパティの型が文字列なら元の型を保持し、そうでなければnever型にする」と指定しています。
ただし、これだけではプロパティが完全に除外されるわけではなく、`age: never` のように型が`never`のプロパティも含まれた型が生成されます。

### 型に基づく変換

```typescript
type Data = {
  name: string;
  count: number;
  isActive: boolean;
}

// プロパティの型に基づいて異なる変換を適用
type Transform<T> = {
  [K in keyof T]: T[K] extends string 
    ? `String: ${T[K]}` 
    : T[K] extends number 
      ? `Number: ${T[K]}` 
      : T[K]
}

type TransformedData = Transform<Data>;
// 結果: { 
//   name: `String: ${string}`; 
//   count: `Number: ${number}`; 
//   isActive: boolean; 
// }
```

この例では、オブジェクト型 `T` のプロパティの型に基づいて、異なる型変換を適用します。文字列型のプロパティは `String:` というプレフィックスを付けた文字列リテラル型に変換し、数値型のプロパティは `Number:` というプレフィックスを付けた文字列リテラル型に変換します。それ以外の型のプロパティはそのままの型を保持します。

:::note warn
### 条件付き型の使用上の注意

条件付き型を多用すると型の計算が複雑になり、TypeScriptコンパイラのパフォーマンスに影響を与える可能性があります。特に深くネストした条件分岐は避け、可能な限りシンプルな構造を保つことを推奨します。
:::

## 実用的な使用例

### 1. APIレスポンス型からフォーム型への変換

APIから取得したデータの型と、ユーザー入力用のフォーム型は異なることが多いです。マッピング型を使って、API型から自動的にフォーム型を生成できます。この例では、読み取り専用のIDや日時フィールドを除外し、残りのフィールドをオプショナルにしています。

```typescript
// APIレスポンスの型
type ApiUser = {
  readonly id: number;
  name: string;
  email: string;
  createdAt: Date;
  updatedAt: Date;
}

// フォーム用の型（IDと日時は除外、他はオプショナル）
type UserForm = {
  [K in keyof ApiUser as K extends 'id' | 'createdAt' | 'updatedAt' 
    ? never 
    : K]?: ApiUser[K]
}
// 結果: { name?: string; email?: string; }
```

### 2. データベースエンティティからDTO型の生成

データベースエンティティにはパスワードハッシュなどの機密情報が含まれることがありますが、外部APIに公開する際にはこれらを除外する必要があります。マッピング型を使って、安全にデータを公開するための `DTO`（Data Transfer Object: 外部へ渡すデータ形式）を自動生成できます。

```typescript
// データベースエンティティ
type UserEntity = {
  id: number;
  name: string;
  email: string;
  passwordHash: string;
  internalNotes: string;
  createdAt: Date;
  updatedAt: Date;
}

// 公開API用のDTO（機密情報を除外）
type UserDto = {
  [K in keyof UserEntity as K extends 'passwordHash' | 'internalNotes' 
    ? never 
    : K]: UserEntity[K]
}
```

### 3. 型安全なイベントハンドラーの生成

イベント駆動アーキテクチャ（イベントをきっかけに処理が実行される仕組み）では、さまざまなイベントとそれに対応するハンドラーがあります。マッピング型とテンプレートリテラル型を組み合わせることで、イベント定義から型安全なイベントハンドラー型を自動生成できます。これにより、イベントのプロパティ変更時に関連するハンドラー型も自動的に更新されます。

```typescript
// イベント定義
type Events = {
  userLogin: { userId: number; timestamp: Date };
  userLogout: { userId: number };
  pageView: { url: string; userId?: number };
}

// 型安全なイベントハンドラー型
type EventHandlers = {
  [K in keyof Events as `on${Capitalize<string & K>}`]: (data: Events[K]) => void
}
// 結果: {
//   onUserLogin: (data: { userId: number; timestamp: Date }) => void;
//   onUserLogout: (data: { userId: number }) => void;
//   onPageView: (data: { url: string; userId?: number }) => void;
// }
```

この例では、イベント名に基づいて `on[イベント名]` という名前の関数を生成し、それぞれの関数が対応するイベントデータを引数として受け取るようにします。
たとえば、`userLogin` というイベントが発生した時は `onUserLogin` という `{ userId: number; timestamp: Date }` 型のデータを受け取る関数になります。
このTypeScriptの書き方を使うことで、もし新しいイベントを追加したり、イベントの内容が変わった場合でも、**対応するハンドラーの型も自動で正しく更新される**ので、型の安全性が保たれます。(型の書き間違いやミスを防げます)

:::note info
#### Capitalize ユーティリティ型
`Capitalize<string>` はTypeScriptの組み込みユーティリティ型（再利用性の高い共通処理や補助的機能をまとめた型）で、文字列型の最初の文字を大文字に変換します。例えば `Capitalize<'userLogin'>` は `'UserLogin'` 型になります。これはTypeScript 4.1で導入された機能で、他にも `Uppercase<string>`、`Lowercase<string>`、`Uncapitalize<string>` などの文字列操作用ユーティリティ型があります。

この例では `Capitalize<string & K>` を使って、イベント名の先頭を大文字にしています。`string & K` という表現は、`K` が文字列型であることを保証するためのものです。
:::

## 実務でのベストプラクティス

### 1. 命名規則の統一

Mapped Typesを使用した型には、一貫した命名規則を適用しましょう。

```typescript
// 推奨される命名パターン
type UserPartial = Partial<User>;                            // 既存型 + 操作内容
type CreateUserDto = Omit<User, 'id'>;                       // 用途 + 既存型 + Dto
type UpdateUserForm = Partial<Pick<User, 'name' | 'email'>>; // 用途 + 既存型 + Form
```

### 2. 型の文書化

複雑なMapped Typesには適切なコメントを付けましょう。

```typescript
/**
 * APIレスポンスからフォーム用の型を生成
 * - IDフィールドを除外
 * - すべてのフィールドをオプショナルに変更
 * - 日時フィールドを文字列型に変換
 */
type ApiToFormType<T extends Record<string, any>> = {
  [K in keyof T as K extends 'id' ? never : K]?: 
    T[K] extends Date ? string : T[K]
}
```

### 3. 再利用可能な汎用型の作成

プロジェクト固有の型変換パターンは、再利用可能な汎用型として定義します。

```typescript
// プロジェクト共通のユーティリティ型
type FormType<T> = Partial<Omit<T, 'id' | 'createdAt' | 'updatedAt'>>;
type CreateType<T> = Omit<T, 'id' | 'createdAt' | 'updatedAt'>;
type UpdateType<T> = Partial<Omit<T, 'id' | 'createdAt'>>;
```

## まとめ

TypeScriptのMapped Typesは、型システムを最大限に活用するための強力なツールであり、既存の型から新しい型を効率的に生成することで、コードの重複を減らし、型の一貫性と保守性を大幅に向上させることができます。基本的な構文から始まり、キーの修飾、名前変換、条件付き型との組み合わせなど、様々なテクニックを習得することで、より表現力豊かな型定義が可能になります。

実務では、APIレスポンスからフォーム型への変換、データベースエンティティからDTOの生成、型安全なイベントハンドラーの実装など、多岐にわたる場面でMapped Typesの恩恵を受けることができます。また、命名規則の統一や適切な文書化、再利用可能な汎用型の作成といったベストプラクティスを取り入れることで、チーム全体の開発効率と型安全性をさらに高めることができるでしょう。

もし記事の内容に間違いがあれば、コメントでご指摘いただけますと幸いです。また、より良い方法や代替手段をご存知の方がいらっしゃいましたら、ぜひ共有していただければと存じます。例えば、より複雑な型変換パターンや、大規模プロジェクトでの効果的な型設計アプローチなど、皆様の知見やベストプラクティスをお聞かせいただければ幸いです。