T-CREATOR

<div />

TypeScriptの高度な型操作を使い方で理解する keyof typeof inferを実例で整理

2026年1月16日
TypeScriptの高度な型操作を使い方で理解する keyof typeof inferを実例で整理

TypeScript の型推論を活かしながら、型安全なコードを書きたい。しかし keyoftypeofinfer の違いや使い分けがわからず、ジェネリクスや Utility Types と組み合わせると推論が崩れてしまう——そんな悩みを持つ方は多いのではないでしょうか。本記事では、これら 3 つの型操作の比較・違い・判断基準を実例で整理し、推論が崩れない書き方の指針をまとめます。

keyof・typeof・infer の違いと使い分け早見表

TypeScript の型操作を理解するうえで、まず keyoftypeofinfer の違いを押さえることが重要です。以下の比較表で、それぞれの特徴を一覧できます。

#演算子役割入力出力主な用途
1keyofオブジェクト型のキーを抽出リテラルユニオン型プロパティ名の制約・Mapped Types
2typeof値から型を生成定数・関数・オブジェクトの型取得
3infer条件付き型で型を推論型パターン推論された型戻り値・引数・要素型の抽出

それぞれの詳細な使い方と判断基準は、後続のセクションで解説します。

検証環境

  • OS: macOS Sonoma 14.5
  • Node.js: 22.13.0
  • TypeScript: 5.7.3
  • 主要パッケージ:
    • @types/node: 22.10.7
  • 検証日: 2026 年 01 月 16 日

背景:TypeScript で高度な型操作が求められる理由

この章では、なぜ TypeScript で高度な型操作が求められるのか、keyoftypeofinfer が生まれた技術的背景を解説します。

TypeScript の型推論だけでは不十分な場面

TypeScript の型推論は優秀ですが、以下のような場面では明示的な型操作が必要になります。

  1. API レスポンスの型を動的に生成したい場合
  2. オブジェクトのキーを型安全に扱いたい場合
  3. ジェネリクスの戻り値から特定の型を抽出したい場合

実際に業務で API クライアントを実装した際、レスポンスの型を手動で定義していたところ、エンドポイントが 50 を超えた時点で型定義のメンテナンスが破綻しました。typeofkeyof を組み合わせて型を自動生成する方式に切り替えたことで、この問題を解決できました。

3 つの型操作が登場した経緯

TypeScript は JavaScript に型を付与する言語ですが、JavaScript の動的な性質をそのまま型で表現するのは困難です。この課題に対応するため、以下の演算子が順次導入されました。

  • keyof(TypeScript 2.1):オブジェクトのキーを型として扱う
  • typeof(TypeScript 初期から):値から型を取得する
  • infer(TypeScript 2.8):条件付き型でパターンマッチする

これらを組み合わせることで、JavaScript の柔軟性を維持しながら型安全性を確保できるようになりました。

つまずきやすい点keyoftypeof は似て見えるが、入力が「型」か「値」かで明確に異なります。また infer は条件付き型の中でしか使えないという制約があります。

課題:型操作を使わない場合に起きる実務上の問題

この章では、型操作を避けた場合に発生するリスクと、any や型アサーションの多用が招く問題を取り上げます。

any と型アサーションによる型安全性の喪失

型操作を避けて any や型アサーションで逃げると、以下のリスクが発生します。

typescript// ❌ 型安全性が失われる例
const config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retryCount: 3,
};

// any を使うと型チェックが効かない
function getConfig(key: string): any {
  return (config as any)[key];
}

const url = getConfig("apiUrl"); // any 型になる
const typo = getConfig("apiurl"); // タイポしても検出されない

上記のコードは実行時エラーの原因になります。keyof typeof を使えば、コンパイル時にタイポを検出できます。

型定義の重複とメンテナンスコスト

型操作を使わない場合、同じ構造の型を複数箇所で定義することになり、変更時の修正漏れが発生しやすくなります。

typescript// ❌ 型定義が重複する例
const statusCodes = {
  success: 200,
  notFound: 404,
  error: 500,
};

// 手動で同じ内容を型定義
type StatusCode = 200 | 404 | 500; // statusCodes と同期が必要

// typeof を使えば自動で同期される
type StatusCodeAuto = (typeof statusCodes)[keyof typeof statusCodes];

つまずきやすい点any は便利だが型安全性を完全に失います。型アサーション(as)の多用は「型の嘘」を生む原因になります。

解決策と判断:keyof・typeof・infer の使い分け

この章では、各演算子の基本的な動作と用途、どの場面でどの演算子を選ぶべきかを解説します。

keyof 演算子の使い方と判断基準

keyof の基本:オブジェクト型からキーを抽出する

keyof 演算子は、オブジェクト型のプロパティ名をリテラルユニオン型として抽出します。「型」に対して作用する点が重要です。

以下のコードは動作確認済みです。

typescript// オブジェクト型の定義
type User = {
  id: number;
  name: string;
  email: string;
};

// keyof でキーを抽出
type UserKeys = keyof User; // "id" | "name" | "email"

// 型安全なプロパティアクセス関数
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: User = { id: 1, name: "田中", email: "tanaka@example.com" };
const name = getProperty(user, "name"); // string 型
// getProperty(user, 'age'); // ❌ コンパイルエラー

K extends keyof T という制約により、存在しないキーを指定するとコンパイルエラーになります。

keyof と Mapped Types の組み合わせ

keyof は Mapped Types と組み合わせることで、型の変換に威力を発揮します。TypeScript の Utility Types である PartialReadonlyPick は、内部で keyof を使用しています。

以下の図は、keyof と Mapped Types の関係を示しています。

mermaidflowchart LR
  A["オブジェクト型 T"] --> B["keyof T"]
  B --> C["リテラルユニオン型"]
  C --> D["Mapped Types<br/>[K in keyof T]"]
  D --> E["変換された型"]

図の説明:オブジェクト型から keyof でキーを抽出し、Mapped Types で各プロパティを変換する流れを示しています。

以下のコードは動作確認済みです。

typescript// Partial の内部実装を理解する
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Readonly の内部実装
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// 実務での活用例:フォームの入力状態を表す型
type FormData = {
  username: string;
  password: string;
  rememberMe: boolean;
};

// 入力中は全フィールドがオプショナル
type PartialFormData = MyPartial<FormData>;
// { username?: string; password?: string; rememberMe?: boolean; }

keyof を選ぶ判断基準

  • オブジェクトのプロパティ名を制約したい
  • Mapped Types で型を変換したい
  • 動的なプロパティアクセスを型安全にしたい

つまずきやすい点keyof は「型」に対して使います(値には使えません)。またインデックスシグネチャを持つ型では string | number が返る場合があります。

typeof 演算子の使い方と判断基準

typeof の基本:値から型を取得する

typeof 演算子は、JavaScript の値から TypeScript の型を生成します。「値」に対して作用する点が keyof との大きな違いです。

以下のコードは動作確認済みです。

typescript// オブジェクトリテラルから型を生成
const endpoints = {
  users: "/api/users",
  posts: "/api/posts",
  comments: "/api/comments",
};

// typeof で型を取得
type Endpoints = typeof endpoints;
// { users: string; posts: string; comments: string; }

// keyof typeof の組み合わせ
type EndpointKeys = keyof typeof endpoints;
// "users" | "posts" | "comments"

as const と typeof の組み合わせ

typeof 単体では、リテラル型ではなく stringnumber に拡大されます。リテラル型を保持したい場合は as const を併用します。

実際に試したところ、as const を忘れてリテラル型が失われるバグを何度か経験しました。この組み合わせは必須と考えてください。

typescript// as const なし:型が拡大される
const statusWithoutConst = {
  success: 200,
  notFound: 404,
  error: 500,
};
type StatusWithoutConst = typeof statusWithoutConst;
// { success: number; notFound: number; error: number; }

// as const あり:リテラル型が保持される
const statusWithConst = {
  success: 200,
  notFound: 404,
  error: 500,
} as const;
type StatusWithConst = typeof statusWithConst;
// { readonly success: 200; readonly notFound: 404; readonly error: 500; }

// リテラル型を活用した型安全な関数
type StatusCode = (typeof statusWithConst)[keyof typeof statusWithConst];
// 200 | 404 | 500

function handleStatus(code: StatusCode): string {
  switch (code) {
    case 200:
      return "Success";
    case 404:
      return "Not Found";
    case 500:
      return "Error";
  }
}

関数の型を typeof で抽出する

関数の型を抽出する場合も typeof を使います。Utility Types の ReturnTypeParameters と組み合わせると、関数の戻り値型や引数型を取得できます。

typescript// 関数定義
function createUser(name: string, age: number) {
  return { id: crypto.randomUUID(), name, age, createdAt: new Date() };
}

// 関数の型を取得
type CreateUserFn = typeof createUser;
// (name: string, age: number) => { id: string; name: string; age: number; createdAt: Date; }

// 戻り値の型を取得
type UserResult = ReturnType<typeof createUser>;
// { id: string; name: string; age: number; createdAt: Date; }

// 引数の型を取得
type CreateUserArgs = Parameters<typeof createUser>;
// [name: string, age: number]

typeof を選ぶ判断基準

  • 定数オブジェクトから型を生成したい
  • 関数の型を取得したい
  • 既存のコードから型定義を作りたい

つまずきやすい点typeof は値に対して使います(型には使えません)。as const を忘れるとリテラル型が失われます。またクラスに typeof を使うとコンストラクタ型が返ります。

infer 演算子の使い方と判断基準

infer の基本:条件付き型で型を推論する

infer は条件付き型(Conditional Types)の中でのみ使用でき、型パターンから特定の部分を抽出します。「パターンマッチング」のような動作をすると考えるとわかりやすいでしょう。

以下の図は、infer の動作を示しています。

mermaidflowchart TD
  A["入力型 T"] --> B{"T extends パターン<infer U>?"}
  B -->|マッチ| C["U を抽出して返す"]
  B -->|不一致| D["never を返す"]

図の説明:条件付き型の中で infer がパターンマッチを行い、マッチした場合に推論された型を返す流れを示しています。

以下のコードは動作確認済みです。

typescript// 配列の要素型を抽出
type ArrayElement<T> = T extends (infer U)[] ? U : never;

type Numbers = number[];
type NumberElement = ArrayElement<Numbers>; // number

type Users = { id: number; name: string }[];
type UserElement = ArrayElement<Users>; // { id: number; name: string }

ReturnType と Parameters の内部実装

TypeScript の Utility Types である ReturnTypeParameters は、infer を使って実装されています。内部実装を理解すると、カスタム型の作成に応用できます。

typescript// ReturnType の内部実装
type MyReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : any;

// Parameters の内部実装
type MyParameters<T extends (...args: any) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never;

// 使用例
function fetchUser(id: string): Promise<{ id: string; name: string }> {
  return fetch(`/api/users/${id}`).then((res) => res.json());
}

type FetchUserReturn = MyReturnType<typeof fetchUser>;
// Promise<{ id: string; name: string }>

type FetchUserParams = MyParameters<typeof fetchUser>;
// [id: string]

Promise の中身を取り出す

非同期処理で頻繁に使うパターンとして、Promise<T> から T を取り出す型があります。業務で API レスポンスの型を扱う際に、このパターンを何度も使いました。

typescript// Promise の解決値を抽出
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

// 使用例
type AsyncResult = Promise<Promise<string>>;
type Result = Awaited<AsyncResult>; // string

// 実務での活用:API クライアントの型定義
const api = {
  getUser: (id: string) => fetch(`/api/users/${id}`).then((r) => r.json()),
  getPosts: () => fetch("/api/posts").then((r) => r.json()),
};

type ApiReturnType<K extends keyof typeof api> = Awaited<
  ReturnType<(typeof api)[K]>
>;

関数の第一引数の型を抽出する

infer を使えば、関数の特定の引数だけを抽出することも可能です。検証の結果、以下のパターンが最も汎用的でした。

typescript// 第一引数の型を抽出
type FirstArg<T> = T extends (first: infer F, ...args: any[]) => any
  ? F
  : never;

// 第二引数の型を抽出
type SecondArg<T> = T extends (
  first: any,
  second: infer S,
  ...args: any[]
) => any
  ? S
  : never;

// 使用例
function updateUser(id: string, data: { name?: string; email?: string }) {
  // 実装
}

type UserId = FirstArg<typeof updateUser>; // string
type UpdateData = SecondArg<typeof updateUser>; // { name?: string; email?: string }

infer を選ぶ判断基準

  • 関数の戻り値型や引数型を抽出したい
  • Promise の中身を取り出したい
  • 配列の要素型を取得したい

つまずきやすい点infer は条件付き型の中でしか使えません。複雑なパターンマッチは可読性が下がるため、ネストした infer は避けたほうがよいでしょう。

具体例:keyof・typeof・infer の組み合わせパターン

この章では、3 つの演算子を組み合わせた実践パターン、型安全な API クライアントの実装方法、イベントエミッターの型定義を紹介します。

型安全な設定オブジェクトの実装

実務で最もよく使うパターンとして、設定オブジェクトの型安全なアクセサがあります。以下は実際にプロジェクトで採用した実装です。

typescript// 設定オブジェクトの定義
const appConfig = {
  api: {
    baseUrl: "https://api.example.com",
    timeout: 5000,
  },
  feature: {
    darkMode: true,
    analytics: false,
  },
} as const;

// 型定義
type AppConfig = typeof appConfig;
type ConfigCategory = keyof AppConfig;
type ConfigKey<C extends ConfigCategory> = keyof AppConfig[C];

// 型安全なゲッター
function getConfig<C extends ConfigCategory, K extends ConfigKey<C>>(
  category: C,
  key: K,
): AppConfig[C][K] {
  return appConfig[category][key];
}

// 使用例
const baseUrl = getConfig("api", "baseUrl"); // "https://api.example.com"(リテラル型)
const darkMode = getConfig("feature", "darkMode"); // true(リテラル型)
// getConfig('api', 'darkMode'); // ❌ コンパイルエラー

型安全なイベントエミッターの実装

以下の実装は、業務でイベント駆動アーキテクチャを採用した際に作成したものです。型安全性と使いやすさのバランスを検証した結果、この形に落ち着きました。

typescript// イベントマップの定義
type EventMap = {
  userCreated: { id: string; name: string };
  userUpdated: {
    id: string;
    changes: Partial<{ name: string; email: string }>;
  };
  userDeleted: { id: string };
};

// イベントエミッターの型定義
type EventEmitter<T extends Record<string, unknown>> = {
  on<K extends keyof T>(event: K, handler: (data: T[K]) => void): void;
  off<K extends keyof T>(event: K, handler: (data: T[K]) => void): void;
  emit<K extends keyof T>(event: K, data: T[K]): void;
};

// 実装
function createEventEmitter<
  T extends Record<string, unknown>,
>(): EventEmitter<T> {
  const handlers = new Map<keyof T, Set<(data: any) => void>>();

  return {
    on(event, handler) {
      if (!handlers.has(event)) handlers.set(event, new Set());
      handlers.get(event)!.add(handler);
    },
    off(event, handler) {
      handlers.get(event)?.delete(handler);
    },
    emit(event, data) {
      handlers.get(event)?.forEach((h) => h(data));
    },
  };
}

// 使用例
const emitter = createEventEmitter<EventMap>();
emitter.on("userCreated", (data) => {
  console.log(data.id, data.name); // 型推論が効く
});
// emitter.emit('userCreated', { id: '1' }); // ❌ name が足りない

Utility Types を自作する

標準の Utility Types で対応できない要件がある場合、keyoftypeofinfer を組み合わせて自作できます。

typescript// 深いネストを Readonly にする
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepReadonly<T[K]>
    : T[K];
};

// null/undefined を除外したキーのみ抽出
type RequiredKeys<T> = {
  [K in keyof T]-?: undefined extends T[K] ? never : K;
}[keyof T];

// 使用例
type User = {
  id: string;
  name: string;
  email?: string;
  age?: number;
};

type UserRequiredKeys = RequiredKeys<User>; // "id" | "name"

推論が崩れないための設計指針

検証中に遭遇した、型推論が崩れるパターンをまとめます。

typescript// ❌ パターン1:as const の付け忘れ
const status = { ok: 200, error: 500 }; // number に拡大される

// ✅ 対策:as const を必ず付ける
const statusFixed = { ok: 200, error: 500 } as const;

// ❌ パターン2:ジェネリクスの制約不足
function getValue<T>(obj: T, key: string) {
  // key が any 扱い
  return (obj as any)[key];
}

// ✅ 対策:keyof で制約する
function getValueFixed<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// ❌ パターン3:過剰な型アサーション
const data = fetchData() as UserData; // 実行時エラーの可能性

// ✅ 対策:型ガードを使う
function isUserData(data: unknown): data is UserData {
  return typeof data === "object" && data !== null && "id" in data;
}

つまずきやすい点:定数オブジェクトには必ず as const を付けること、ジェネリクスには適切な制約(extends)を設定すること、型アサーションより型ガードを優先することが重要です。

比較まとめ:keyof・typeof・infer の詳細な判断基準

改めて 3 つの型操作を比較します。冒頭の簡易表よりも詳細な判断基準を含めています。

観点keyoftypeofinfer
入力型パターン
出力リテラルユニオン型推論された型
使用場所型定義全般型定義全般条件付き型の中のみ
主な用途キー制約・Mapped Types値からの型生成型の抽出・分解
組み合わせtypeof, Mapped Typesas const, keyofextends, 条件付き型
学習コスト中〜高
可読性への影響中〜高

使い分けの判断フロー

以下のフローで判断すると迷いが少なくなります。

mermaidflowchart TD
  A["型操作が必要"] --> B{"既存の値から型を作りたい?"}
  B -->|はい| C["typeof を使う"]
  B -->|いいえ| D{"オブジェクト型のキーが欲しい?"}
  D -->|はい| E["keyof を使う"]
  D -->|いいえ| F{"型の一部を抽出したい?"}
  F -->|はい| G["infer を使う"]
  F -->|いいえ| H["Utility Types を検討"]

図の説明:型操作の選択を判断するフローチャートです。

向いているケース・向かないケース

keyof が向いているケース

  • オブジェクトのプロパティ名を制約したい
  • Mapped Types で型を変換したい
  • 動的なプロパティアクセスを型安全にしたい

keyof が向かないケース

  • 値(変数)から直接キーを取りたい(typeof と併用が必要)

typeof が向いているケース

  • 定数オブジェクトから型を生成したい
  • 関数の型を取得したい
  • 既存のコードから型定義を作りたい

typeof が向かないケース

  • 型に対して使いたい(型には使えない)

infer が向いているケース

  • 関数の戻り値型や引数型を抽出したい
  • Promise の中身を取り出したい
  • 配列の要素型を取得したい

infer が向かないケース

  • シンプルな型変換(Utility Types で十分)
  • 可読性を重視するプロジェクト(複雑になりやすい)

まとめ

TypeScript の keyoftypeofinfer は、型安全なコードを書くための強力なツールです。それぞれの役割は明確に異なります。

  • keyof:オブジェクト型のキーをリテラルユニオン型として抽出
  • typeof:JavaScript の値から TypeScript の型を生成
  • infer:条件付き型の中で型パターンから特定の部分を推論

これらを適切に組み合わせることで、API クライアント、イベントエミッター、設定オブジェクトなど、実務で求められる型安全な実装が可能になります。

ただし、複雑な型操作は可読性を下げる要因にもなります。チームの TypeScript 習熟度やプロジェクトの性質に応じて、適切な抽象度を選択してください。

関連リンク

著書

とあるクリエイター

フロントエンドエンジニア Next.js / React / TypeScript / Node.js / Docker / AI Coding

;