7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[TypeScriptシリーズ - Part 2] Mapped Types

7
Posted at

[TypeScriptシリーズ - Part 2] Mapped Types

📝 注記
私は日本語が得意ではありません。この記事はAIのサポートを受けて書いています。ご了承ください。


📖 目次

  1. 問題の提示 – どんな時にこのテクニックが必要か
  2. 悪い例 – まずはダメなコードを見せる
  3. 良い例 – TypeScriptの高度機能で解決する
  4. Playgroundリンク – その場で試せる
  5. 課題 – シニア向けのチャレンジ問題
  6. まとめ

1. 問題の提示 – どんな時にこのテクニックが必要か

あなたは大規模なフォームシステムを管理しています。以下のような複数の設定オブジェクトがあります。

interface UserSettings {
  darkMode: boolean;
  notifications: boolean;
  language: string;
}

interface EditorSettings {
  fontSize: number;
  fontFamily: string;
  showLineNumbers: boolean;
  autoSave: boolean;
}

問題点:

  • 各設定の「フラグバージョン」(全てbooleanに変換した型)が必要
  • 各設定の「読み取り専用バージョン」が必要
  • 各設定の「オプショナルバージョン」が必要
  • 設定が増えるたびに、同じ変換を手動で繰り返し定義している

問いかけ:

どうすれば、ある型から別の型への変換を一括で自動化できるでしょうか?


2. 悪い例 – まずはダメなコードを見せる

// ❌ 毎回手動で変換用の型を定義

// UserSettingsのフラグバージョン
interface UserSettingsFlags {
  darkMode: boolean;
  notifications: boolean;
  language: boolean;  // string → boolean
}

// EditorSettingsのフラグバージョン
interface EditorSettingsFlags {
  fontSize: boolean;   // number → boolean
  fontFamily: boolean; // string → boolean
  showLineNumbers: boolean;
  autoSave: boolean;
}

// UserSettingsの読み取り専用バージョン
interface UserSettingsReadonly {
  readonly darkMode: boolean;
  readonly notifications: boolean;
  readonly language: string;
}

// EditorSettingsの読み取り専用バージョン
interface EditorSettingsReadonly {
  readonly fontSize: number;
  readonly fontFamily: string;
  readonly showLineNumbers: boolean;
  readonly autoSave: boolean;
}

// UserSettingsのオプショナルバージョン
interface UserSettingsPartial {
  darkMode?: boolean;
  notifications?: boolean;
  language?: string;
}

// EditorSettingsのオプショナルバージョン
interface EditorSettingsPartial {
  fontSize?: number;
  fontFamily?: string;
  showLineNumbers?: boolean;
  autoSave?: boolean;
}

// 新しい設定が増えるたびに、同じパターンを繰り返す 😰

なぜ悪いのか:

問題 説明
非DRY 同じ変換ロジックを何度も書き直す
エラーが起きやすい プロパティ名のタイプミスや漏れが発生
保守性が低い 元の型にプロパティを追加するとき、全変換型も手動更新が必要
スケールしない 設定が10個、20個と増えると破綻する

3. 良い例 – TypeScriptの高度機能で解決する

基本: Mapped Typesとは?

Mapped Typesは、既存の型のプロパティを反復処理して新しい型を作成します。

// 基本構文
type MappedType<T> = {
  [Property in keyof T]: 新しい型
};

ユースケース1: 全てのプロパティをbooleanに変換

type Flags<T> = {
  [Property in keyof T]: boolean;
};

type UserSettings = {
  darkMode: boolean;
  notifications: boolean;
  language: string;
};

type UserFlags = Flags<UserSettings>;
// 結果: { darkMode: boolean; notifications: boolean; language: boolean }

type EditorSettings = {
  fontSize: number;
  fontFamily: string;
  showLineNumbers: boolean;
  autoSave: boolean;
};

type EditorFlags = Flags<EditorSettings>;
// 結果: { fontSize: boolean; fontFamily: boolean; showLineNumbers: boolean; autoSave: boolean }

ユースケース2: 全てのプロパティを読み取り専用に

type ReadonlyDeep<T> = {
  readonly [Property in keyof T]: T[Property];
};

type UserReadonly = ReadonlyDeep<UserSettings>;
// 結果: { readonly darkMode: boolean; readonly notifications: boolean; readonly language: string }

ユースケース3: 全てのプロパティをオプショナルに

type Partial<T> = {
  [Property in keyof T]?: T[Property];
};

type UserPartial = Partial<UserSettings>;
// 結果: { darkMode?: boolean; notifications?: boolean; language?: string }

ユースケース4: 修飾子の削除(-readonly-?

// 読み取り専用を解除
type Mutable<T> = {
  -readonly [Property in keyof T]: T[Property];
};

// オプショナルを解除(必須に変更)
type Required<T> = {
  [Property in keyof T]-?: T[Property];
};

type LockedUser = {
  readonly id: string;
  readonly name: string;
  age?: number;
};

type UnlockedUser = Mutable<LockedUser>;
// 結果: { id: string; name: string; age?: number }

type RequiredUser = Required<LockedUser>;
// 結果: { readonly id: string; readonly name: string; age: number }

ユースケース5: キーのリマッピング(TypeScript 4.1+)

// 全てのプロパティにgetterを追加
type Getters<T> = {
  [Property in keyof T as `get${Capitalize<string & Property>}`]: () => T[Property];
};

interface Person {
  name: string;
  age: number;
  location: string;
}

type LazyPerson = Getters<Person>;
// 結果: {
//   getName: () => string;
//   getAge: () => number;
//   getLocation: () => string;
// }

ユースケース6: 特定のキーをフィルタリング

// 'id'プロパティを除外
type ExcludeId<T> = {
  [Property in keyof T as Exclude<Property, 'id'>]: T[Property];
};

interface User {
  id: string;
  name: string;
  email: string;
  password: string;
}

type UserWithoutId = ExcludeId<User>;
// 結果: { name: string; email: string; password: string }

ユースケース7: Conditional Typesと組み合わせ

// PII(個人情報)フラグがあるプロパティを検出
type ExtractPII<T> = {
  [Property in keyof T]: T[Property] extends { pii: true } ? true : false;
};

type DBFields = {
  id: { format: "incrementing" };
  name: { type: string; pii: true };
  email: { type: string; pii: true };
  age: { type: number };
};

type NeedsGDPRDeletion = ExtractPII<DBFields>;
// 結果: { id: false; name: true; email: true; age: false }

処理の流れ(視覚モデル)

Flags<UserSettings>
    │
    ▼
{ [Property in keyof UserSettings]: boolean }
    │
    ▼
keyof UserSettings = "darkMode" | "notifications" | "language"
    │
    ▼
{
  darkMode: boolean;
  notifications: boolean;
  language: boolean;
}
    │
    ▼
UserFlags ✅

4. Playgroundリンク – その場で試せる

理論だけでは実感しにくいので、実際に動かして確認してみましょう。
TypeScript Playgroundはブラウザ上でTypeScriptを実行できる公式ツールです。インストール不要、すぐに試せます。

🔗 Playground URL: https://www.typescriptlang.org/play/

何を確認できるのか?

下のコードをコピーしてPlaygroundに貼り付けた後、各 type の名前にマウスをホバーしてみてください。TypeScriptが自動で変換した型がポップアップで表示されます。

// ① このコードをコピーしてPlaygroundに貼り付ける
type Flags<T> = {
  [Property in keyof T]: boolean;
};

type ReadonlyDeep<T> = {
  readonly [Property in keyof T]: T[Property];
};

type Mutable<T> = {
  -readonly [Property in keyof T]: T[Property];
};

type Required<T> = {
  [Property in keyof T]-?: T[Property];
};

type Getters<T> = {
  [Property in keyof T as `get${Capitalize<string & Property>}`]: () => T[Property];
};

type ExcludeId<T> = {
  [Property in keyof T as Exclude<Property, 'id'>]: T[Property];
};

interface UserSettings {
  darkMode: boolean;
  notifications: boolean;
  language: string;
}

interface LockedUser {
  readonly id: string;
  readonly name: string;
  age?: number;
}

interface Person {
  name: string;
  age: number;
  location: string;
}

interface User {
  id: string;
  name: string;
  email: string;
  password: string;
}

// ② これらの型名にマウスをホバーして確認しよう
type UserFlags = Flags<UserSettings>;
type UserReadonly = ReadonlyDeep<UserSettings>;
type UnlockedUser = Mutable<LockedUser>;
type RequiredUser = Required<LockedUser>;
type LazyPerson = Getters<Person>;
type UserWithoutId = ExcludeId<User>;

ホバーすると何が見える?

例えば UserFlags にホバーすると:

type UserFlags = {
  darkMode: boolean;
  notifications: boolean;
  language: boolean;
}

元の language: stringlanguage: boolean に自動変換されているのが確認できます。これがMapped Typesの力です。


5. 課題 – シニア向けのチャレンジ問題

課題1: Asyncify – 全てのプロパティをPromiseに変換

以下のインターフェースがあります:

interface ApiService {
  getUser: (id: string) => User;
  getProduct: (id: string) => Product;
  saveOrder: (order: Order) => void;
}

Asyncify<T> を実装して、全てのメソッドの戻り値の型を Promise<T> に変換してください。

💡 ヒント: Mapped Types + infer を組み合わせます。

✅ 解答を見る(クリック)
type Asyncify<T> = {
  [Property in keyof T]: T[Property] extends (...args: infer A) => infer R
    ? (...args: A) => Promise<R>
    : T[Property];
};

type AsyncApiService = Asyncify<ApiService>;
// 結果: {
//   getUser: (id: string) => Promise<User>;
//   getProduct: (id: string) => Promise<Product>;
//   saveOrder: (order: Order) => Promise<void>;
// }

課題2: Nullable – 全てのプロパティをnullableに変換

以下のインターフェースがあります:

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

Nullable<T> を作成し、全てのプロパティの型を T | null に変換してください。

✅ 解答を見る(クリック)
type Nullable<T> = {
  [Property in keyof T]: T[Property] | null;
};

type NullableUser = Nullable<User>;
// 結果: { id: string | null; name: string | null; email: string | null }

課題3: DeepReadonly – ネストされたオブジェクトも読み取り専用に

以下のような深いネスト構造があるとします:

interface NestedObject {
  a: number;
  b: {
    c: string;
    d: {
      e: boolean;
    };
  };
}

DeepReadonly<T> を作成し、全てのネストレベルreadonly を適用してください。

💡 ヒント: 再帰的なMapped Typesを使います。

✅ 解答を見る(クリック)
type DeepReadonly<T> = {
  readonly [Property in keyof T]: DeepReadonly<T[Property]>;
};

type ReadonlyNested = DeepReadonly<NestedObject>;
// 結果: {
//   readonly a: number;
//   readonly b: {
//     readonly c: string;
//     readonly d: {
//       readonly e: boolean;
//     };
//   };
// }

課題4(ボーナス): EventConfig – イベントハンドラの型生成

以下のイベント型があるとします:

type SquareEvent = { kind: "square"; x: number; y: number };
type CircleEvent = { kind: "circle"; radius: number };
type MouseEvent  = { kind: "click"; x: number; y: number };

EventConfig<T> を作成し、kind プロパティをキー、イベントオブジェクト全体を引数に持つ関数を値とする型を生成してください。

💡 ヒント: キーのリマッピング(as 句)を使います。

✅ 解答を見る(クリック)
type EventConfig<T extends { kind: string }> = {
  [E in T as E["kind"]]: (event: E) => void;
};

type Config = EventConfig<SquareEvent | CircleEvent | MouseEvent>;
// 結果: {
//   square: (event: SquareEvent) => void;
//   circle: (event: CircleEvent) => void;
//   click:  (event: MouseEvent) => void;
// }

6. まとめ

今日学んだこと

技術 説明
Mapped Types 基本 [Property in keyof T] – プロパティを反復処理
修飾子の追加 readonly? を追加
修飾子の削除 -readonly-? で削除
キーリマッピング as 句でプロパティ名を変換
フィルタリング never を返してキーを除外
Conditional Types連携 値の型に応じて条件分岐

シニアへのアドバイス

同じような型変換を手動で書いている自分に気づいたら、それはMapped Typesの出番です。
ただし、過度に複雑なMapped Typesは可読性を下げます。チームが理解できる範囲で使いましょう。

Have a nice day! 🚀

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?