【TypeScript】ジェネリクス(Generics)完全ガイド|基礎から実務パターンまで徹底解説

【TypeScript】ジェネリクス(Generics)完全ガイド|基礎から実務パターンまで徹底解説 TypeScript

TypeScriptのジェネリクス(Generics)は、型を「引数」として受け取ることで、あらゆる型に対応しつつ型安全性を保つ仕組みです。関数・クラス・インターフェース・型エイリアスに適用でき、再利用性と保守性の高いコードを書くうえで欠かせない機能です。

any を使えば型エラーは消えるけど、それでは型チェックの意味がない」「同じロジックなのに型が違うだけで関数を何個も書くのはつらい」――ジェネリクスは、まさにこうした課題を型システムの力で解決するための武器です。

この記事では、ジェネリクスの基本構文から始めて、型制約(extends)keyofConditional TypesMapped TypesTemplate Literal TypesVariadic Tuple Types再帰型といった高度なトピック、さらにはユーティリティ型の内部実装実務パターン集まで、ジェネリクスのすべてを体系的に解説します。

この記事で学べること

  • ジェネリクスとは何か ― any との決定的な違い
  • 関数・インターフェース・クラス・型エイリアスでの基本構文
  • 型引数の命名規則(T, U, K, V, E, R)とその慣例
  • 型制約(extends)で型パラメータを絞り込む方法
  • keyof とジェネリクスの組み合わせによるプロパティ安全アクセス
  • 複数の型パラメータとデフォルト型パラメータ
  • Conditional Types(条件付き型)と infer キーワード
  • Mapped Types でオブジェクト型を動的に変換する方法
  • Template Literal Types で文字列リテラル型を操作する方法
  • ジェネリクスの型推論のしくみ ― いつ明示が必要か
  • Variadic Tuple Types(可変長タプル型)
  • 再帰型(Recursive Types)による複雑なデータ構造の型定義
  • Partial, Pick, Omit, Record 等のユーティリティ型を自作して理解する
  • 実務パターン集(型安全な API Client / Event Emitter / State Manager / Form Validator / DI Container)
  • ジェネリクスのアンチパターンと落とし穴
  • よくあるエラーメッセージと対処法

前提知識:この記事はTypeScriptの基本型・関数の型定義・クラスの型定義を理解している方を対象としています。基本型から学びたい方は【TypeScript】型の書き方 完全入門を、関数特化で学びたい方は【TypeScript】関数の型定義 完全ガイドを、クラス特化で学びたい方は【TypeScript】クラスの型定義 完全ガイドを先にお読みください。

スポンサーリンク
  1. ジェネリクスとは何か ― なぜ必要なのか
    1. any を使った場合の問題点
    2. ジェネリクスで型安全に解決する
    3. any とジェネリクスの比較
  2. ジェネリクスの基本構文 ― 関数・インターフェース・クラス・型エイリアス
    1. ジェネリクス関数
    2. ジェネリクスインターフェース
    3. ジェネリクスクラス
    4. ジェネリクス型エイリアス
    5. 構文まとめ
  3. 型引数の命名規則 ― T, U, K, V, E, R の慣例
  4. 型制約(extends)の使い方 ― 型パラメータを絞り込む
    1. 基本的な型制約
    2. オブジェクト型の制約
    3. インターフェースを使った制約
    4. 複数の制約を組み合わせる
  5. keyof とジェネリクスの組み合わせ ― プロパティ安全アクセス
    1. keyof の基本
    2. keyof + ジェネリクスで型安全な getProperty
    3. 型安全な setProperty
    4. keyof + ジェネリクスの実践例: pluck関数
  6. 複数の型パラメータ ― 型同士の関係を表現する
    1. 基本: 2つの型パラメータ
    2. 型パラメータ間の制約
    3. 3つ以上の型パラメータ
  7. デフォルト型パラメータ ― 型引数の省略を可能にする
    1. 基本構文
    2. 制約とデフォルトの組み合わせ
    3. React コンポーネントでの活用
  8. Conditional Types(条件付き型)と infer
    1. 基本構文
    2. infer キーワード ― 型を抽出する
    3. 分配条件型(Distributive Conditional Types)
    4. Conditional Types の実践的なパターン
    5. infer の位置による抽出パターン一覧
  9. Mapped Types とジェネリクス ― オブジェクト型を動的に変換する
    1. 基本構文
    2. Mapped Types の修飾子
    3. Key Remapping(as によるキー変換)
  10. Template Literal Types とジェネリクス ― 文字列リテラル型を操作する
    1. 基本構文
    2. 組み込み文字列操作型
    3. ジェネリクスとの組み合わせ: イベントハンドラー型
    4. Template Literal Types で文字列パターンを解析する
  11. ジェネリクスの型推論のしくみ ― いつ明示が必要か
    1. 推論が成功するケース
    2. 推論が期待通りにいかないケース
    3. 型推論ルールまとめ
  12. Variadic Tuple Types(可変長タプル型)
    1. 基本構文
    2. 実践例: 型安全な関数合成
  13. 再帰型(Recursive Types)― 複雑なデータ構造の型定義
    1. 基本的な再帰型
    2. ジェネリクスを使った再帰的な型操作
    3. 再帰的なパス型
  14. ユーティリティ型の内部実装を読み解く ― 自作して理解する
    1. Partial<T> ― すべてのプロパティをオプショナルに
    2. Pick<T, K> ― 特定のプロパティだけを抽出
    3. Omit<T, K> ― 特定のプロパティを除外
    4. Record<K, V> ― キーと値の型を指定したオブジェクト
    5. カスタムユーティリティ型を自作する
    6. ユーティリティ型チートシート
  15. 実務パターン集 ― ジェネリクスを活かす実践的な設計
    1. パターン1: 型安全な API Client
    2. パターン2: 型安全な Event Emitter
    3. パターン3: 型安全な State Manager
    4. パターン4: 型安全な Form Validator
    5. パターン5: 型安全な DI Container
  16. ジェネリクスのアンチパターンと落とし穴
    1. アンチパターン1: 不要なジェネリクス
    2. アンチパターン2: extends any / unknown の無意味な制約
    3. アンチパターン3: 過度に複雑なジェネリクス
    4. アンチパターン4: as 型アサーションでジェネリクスをバイパス
    5. アンチパターン5: 型パラメータの命名が不適切
    6. アンチパターンまとめ
  17. よくあるエラーメッセージと対処法
    1. エラー1: Type ‘X’ is not assignable to type ‘T’
    2. エラー2: Type ‘T’ does not satisfy the constraint
    3. エラー3: Property ‘X’ does not exist on type ‘T’
    4. エラー4: Type instantiation is excessively deep and possibly infinite
    5. エラーメッセージ早見表
  18. まとめ

ジェネリクスとは何か ― なぜ必要なのか

ジェネリクス(Generics)とは、型をパラメータ化する仕組みです。関数の引数が「値」を受け取るように、ジェネリクスは「型」を受け取ります。これにより、特定の型に依存しない汎用的なコードを書きつつ、呼び出し時には具体的な型で型チェックが行われます。

any を使った場合の問題点

「型を気にしなくていい関数」を書きたいとき、最も安直な方法は any を使うことです。しかし、any は型チェックを完全に放棄してしまいます。

any を使った汎用関数(問題あり)
function firstElement(arr: any[]): any {
  return arr[0];
}

const num = firstElement([1, 2, 3]);
// num の型は any → number であることが失われている

const str = firstElement(['hello', 'world']);
// str の型も any → string であることが失われている

// any なので、存在しないメソッドを呼んでもエラーにならない
num.toUpperCase();  // コンパイルエラーなし!実行時エラー!

ジェネリクスで型安全に解決する

ジェネリクスを使えば、入力の型と出力の型の関係を型システムに伝えられます。

ジェネリクスを使った汎用関数(型安全)
function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

const num = firstElement([1, 2, 3]);
// num の型は number | undefined → 型が保持されている!

const str = firstElement(['hello', 'world']);
// str の型は string | undefined → 型が保持されている!

num.toUpperCase();
// コンパイルエラー! number に toUpperCase は存在しない

ポイント:<T> が「型引数」です。関数が呼ばれたとき、TypeScript は渡された引数の型から T を自動的に推論します。Tnumber と推論されれば、戻り値も number | undefined になります。これが「型の流れを保つ」ということです。

any とジェネリクスの比較

観点 any ジェネリクス(T)
型チェック 無効(すべて許可) 有効(型が保持される)
型推論 常に any 呼び出し時の引数から自動推論
IDE補完 効かない 正確に効く
入出力の型関係 失われる 保持される
リファクタリング安全性 低い 高い
実行時エラーリスク 高い 低い

注意:unknown も「任意の型を受け入れる」点では似ていますが、unknown は型ガードなしにはメンバーアクセスできないため、any より安全です。ただし、入出力の型関係を保つ用途ではジェネリクスを使います。

ジェネリクスの基本構文 ― 関数・インターフェース・クラス・型エイリアス

ジェネリクスは、TypeScript の主要な構造すべてで使えます。ここではそれぞれの基本的な書き方を確認しましょう。

ジェネリクス関数

最も基本的なジェネリクスの使い方は、関数に型パラメータを付けることです。

ジェネリクス関数の基本
// function宣言
function identity<T>(value: T): T {
  return value;
}

// アロー関数
const identity2 = <T>(value: T): T => value;

// 呼び出し(型推論)
const a = identity(42);          // T = number と推論 → a: number
const b = identity('hello');     // T = string と推論 → b: string

// 呼び出し(明示的に型を指定)
const c = identity<boolean>(true); // T = boolean を明示 → c: boolean

注意:TSXファイル(React)でアロー関数のジェネリクスを書くと、<T> が JSX タグと解釈されることがあります。回避方法は <T,>(末尾カンマ)または <T extends unknown> です。

TSXでのアロー関数ジェネリクス
// NG: TSX で <T> が JSXタグと誤認される
const identity = <T>(value: T): T => value;

// OK: 末尾カンマで回避
const identity = <T,>(value: T): T => value;

// OK: extends unknown で回避
const identity = <T extends unknown>(value: T): T => value;

ジェネリクスインターフェース

インターフェースに型パラメータを付けると、さまざまな型のデータ構造を一つの定義で表現できます。

ジェネリクスインターフェース
// APIレスポンスの共通構造
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: Date;
}

// ユーザー情報のレスポンス
interface User {
  id: number;
  name: string;
  email: string;
}

const userResponse: ApiResponse<User> = {
  data: { id: 1, name: 'Alice', email: 'alice@example.com' },
  status: 200,
  message: 'Success',
  timestamp: new Date()
};

// 商品リストのレスポンス
interface Product {
  id: number;
  name: string;
  price: number;
}

const productResponse: ApiResponse<Product[]> = {
  data: [{ id: 1, name: 'Laptop', price: 1200 }],
  status: 200,
  message: 'Success',
  timestamp: new Date()
};

ジェネリクスクラス

クラスに型パラメータを付けると、さまざまな型を扱うデータ構造やコンテナを型安全に実装できます。

ジェネリクスクラス
class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get size(): number {
    return this.items.length;
  }
}

// number のスタック
const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
numStack.push('hello'); // エラー! string は number に代入できない

// string のスタック
const strStack = new Stack<string>();
strStack.push('TypeScript');
const top = strStack.pop(); // top: string | undefined

ジェネリクス型エイリアス

型エイリアス(type)でもジェネリクスが使えます。オブジェクト型だけでなく、ユニオン型やタプル型と組み合わせることで、柔軟な型定義が可能です。

ジェネリクス型エイリアス
// 成功 or 失敗を表す Result 型
type Result<T, E> =
  | { success: true; data: T }
  | { success: false; error: E };

// 使用例
function divide(a: number, b: number): Result<number, string> {
  if (b === 0) {
    return { success: false, error: 'Division by zero' };
  }
  return { success: true, data: a / b };
}

const result = divide(10, 3);
if (result.success) {
  console.log(result.data);  // data: number(型が絞り込まれる)
} else {
  console.log(result.error); // error: string(型が絞り込まれる)
}

// Nullable型
type Nullable<T> = T | null;

// ペア型
type Pair<T, U> = [T, U];

const nameAge: Pair<string, number> = ['Alice', 30];

ポイント:interfacetype のどちらでジェネリクスを使うかは、一般的なルールと同じです。オブジェクトの構造を定義するなら interface、ユニオン型やタプル型と組み合わせるなら type が適しています。

構文まとめ

構造 構文
関数宣言 function f<T>(...) function identity<T>(v: T): T
アロー関数 const f = <T>(...) => ... const id = <T,>(v: T): T => v
インターフェース interface I<T> { ... } interface Box<T> { value: T }
クラス class C<T> { ... } class Stack<T> { ... }
型エイリアス type T<U> = ... type Result<T, E> = ...

型引数の命名規則 ― T, U, K, V, E, R の慣例

ジェネリクスの型パラメータには任意の名前を付けられますが、コミュニティで広く使われている慣例があります。これに従うことで、コードの可読性が大幅に向上します。

名前 意味 代表的な使用場面
T Type(型) 最も一般的な型パラメータ Array<T>, Promise<T>
U, V 2番目、3番目の型 複数の型パラメータがあるとき Map<T, U>
K Key(キー) オブジェクトのキー型 Record<K, V>
V Value(値) オブジェクトの値型 Map<K, V>
E Element / Error 配列要素やエラー型 Result<T, E>
R Return(戻り値) 関数の戻り値型 (...args: any[]) => R
P Props / Params Reactコンポーネントのprops型 FC<P>
S State 状態管理の型 Store<S, A>
命名規則の実例
// T: 一般的な型パラメータ
function toArray<T>(value: T): T[] {
  return [value];
}

// K, V: キーと値
function mapEntries<K, V>(map: Map<K, V>): [K, V][] {
  return [...map.entries()];
}

// T, E: 成功データとエラー
type AsyncResult<T, E = Error> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: E };

// 意味が明確なら、具体的な名前もOK
type EventHandler<EventType, ReturnType> = (
  event: EventType
) => ReturnType;

ポイント:型パラメータが1〜2個なら T, U のような1文字が簡潔です。3個以上になったり、意味を明確にしたい場合は TKey, TValue のようなTプレフィックス + 意味のある名前が読みやすくなります。

型制約(extends)の使い方 ― 型パラメータを絞り込む

ジェネリクスの型パラメータは、デフォルトではあらゆる型を受け入れます。しかし実際には「文字列か数値だけ」「特定のプロパティを持つオブジェクトだけ」といった制約を付けたい場面が多くあります。extends キーワードを使うと、型パラメータに上限境界(Upper Bound)を設定できます。

基本的な型制約

extends による型制約
// T を number | string に制約
function add<T extends number | string>(a: T, b: T): T {
  return (a as any) + (b as any);
}

add(1, 2);           // OK: T = number
add('a', 'b');       // OK: T = string
add(true, false);    // エラー! boolean は number | string に代入できない

オブジェクト型の制約

最も実用的なのは、「特定のプロパティを持つオブジェクト」という制約です。

オブジェクト型の制約
// length プロパティを持つ型に制約
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(value: T): T {
  console.log(`Length: ${value.length}`);
  return value;
}

logLength('hello');        // OK: string は length を持つ
logLength([1, 2, 3]);      // OK: 配列は length を持つ
logLength({ length: 10 }); // OK: length プロパティを持つオブジェクト
logLength(42);             // エラー! number に length はない

インターフェースを使った制約

より実務的な例として、「IDを持つエンティティ」に制約する関数を作ってみましょう。

インターフェースによる制約
interface Entity {
  id: number;
  createdAt: Date;
}

// Entity を継承(実装)した型のみ受け入れる
function findById<T extends Entity>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id);
}

interface User extends Entity {
  name: string;
  email: string;
}

interface Product extends Entity {
  name: string;
  price: number;
}

const users: User[] = [
  { id: 1, name: 'Alice', email: 'alice@test.com', createdAt: new Date() },
  { id: 2, name: 'Bob', email: 'bob@test.com', createdAt: new Date() },
];

const user = findById(users, 1);
// user の型は User | undefined(Entity ではなく User として推論される!)

ポイント:T extends Entity により、TEntityサブタイプに制限されます。重要なのは、戻り値が Entity ではなく T(= 具体的な型、例えば User)として推論されることです。これがジェネリクスの強みです。

複数の制約を組み合わせる

交差型(&)を使えば、複数の制約を同時に適用できます。

複数の制約(交差型)
interface Identifiable {
  id: number;
}

interface Nameable {
  name: string;
}

// id と name の両方を持つ型のみ許可
function displayEntity<T extends Identifiable & Nameable>(entity: T): string {
  return `#${entity.id}: ${entity.name}`;
}

displayEntity({ id: 1, name: 'Alice' });           // OK
displayEntity({ id: 1, name: 'Alice', age: 30 });  // OK(余分なプロパティも可)
displayEntity({ id: 1 });                       // エラー! name がない

keyof とジェネリクスの組み合わせ ― プロパティ安全アクセス

keyof 演算子は、オブジェクト型のすべてのキーをユニオン型として取得します。これをジェネリクスと組み合わせると、「オブジェクトに存在するプロパティのみアクセスを許可する」という強力な型安全パターンが実現できます。

keyof の基本

keyof の基本
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// keyof User = "id" | "name" | "email" | "age"
type UserKeys = keyof User;

const key1: UserKeys = 'name';     // OK
const key2: UserKeys = 'address';  // エラー! "address" は存在しない

keyof + ジェネリクスで型安全な getProperty

TypeScript 公式ドキュメントでも紹介されている代表的なパターンです。

型安全な getProperty
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: User = { id: 1, name: 'Alice', email: 'alice@test.com', age: 30 };

const name = getProperty(user, 'name');    // string(User["name"] の型)
const age = getProperty(user, 'age');      // number(User["age"] の型)
const bad = getProperty(user, 'address'); // エラー! "address" は keyof User にない

T[K] はインデックスアクセス型

T[K]インデックスアクセス型(Indexed Access Type)と呼ばれ、「型 T のキー K に対応するプロパティの型」を表します。User["name"]string になり、User["age"]number になります。ジェネリクスと組み合わせると、キーに応じて戻り値の型が自動的に変わる関数を作れます。

型安全な setProperty

同様のパターンで、プロパティの書き込みも型安全にできます。

型安全な setProperty
function setProperty<T, K extends keyof T>(
  obj: T,
  key: K,
  value: T[K]
): void {
  obj[key] = value;
}

setProperty(user, 'name', 'Bob');    // OK: "name" の型は string
setProperty(user, 'age', 25);        // OK: "age" の型は number
setProperty(user, 'age', 'twenty'); // エラー! string は number に代入できない

keyof + ジェネリクスの実践例: pluck関数

配列から特定のプロパティだけを抽出する pluck 関数は、keyof とジェネリクスの典型的な活用パターンです。

pluck 関数
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map(item => item[key]);
}

const users: User[] = [
  { id: 1, name: 'Alice', email: 'a@test.com', age: 30 },
  { id: 2, name: 'Bob', email: 'b@test.com', age: 25 },
];

const names = pluck(users, 'name');  // string[]
const ages = pluck(users, 'age');    // number[]
const bad = pluck(users, 'phone');   // エラー! "phone" は keyof User にない

複数の型パラメータ ― 型同士の関係を表現する

ジェネリクスでは、複数の型パラメータを使うことで、引数や戻り値の型の間の関係を精密に表現できます。

基本: 2つの型パラメータ

2つの型パラメータ
// 2つの値を変換して返す
function map<T, U>(value: T, fn: (v: T) => U): U {
  return fn(value);
}

const result1 = map(42, n => n.toString());
// T = number, U = string → result1: string

const result2 = map('hello', s => s.length);
// T = string, U = number → result2: number

const result3 = map({ x: 1, y: 2 }, p => [p.x, p.y]);
// T = { x: number, y: number }, U = number[] → result3: number[]

型パラメータ間の制約

型パラメータ同士で extends を使い、関係を表現することもできます。

型パラメータ間の制約
// U は T のサブタイプ(T を拡張した型)でなければならない
function assign<T, U extends T>(target: T, source: U): T & U {
  return { ...target, ...source };
}

// K は T のキーに制約される
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const result = {} as Pick<T, K>;
  keys.forEach(key => {
    result[key] = obj[key];
  });
  return result;
}

const user = { id: 1, name: 'Alice', email: 'a@test.com', age: 30 };
const nameAndEmail = pick(user, ['name', 'email']);
// nameAndEmail の型: Pick<User, "name" | "email">
// = { name: string; email: string }

3つ以上の型パラメータ

型パラメータは3つ以上使うこともできますが、あまり多くなると読みにくくなります。実務では3つが実用的な上限です。

3つの型パラメータ
// T: 入力型, U: 中間型, V: 出力型
function pipeline<T, U, V>(
  value: T,
  step1: (v: T) => U,
  step2: (v: U) => V
): V {
  return step2(step1(value));
}

const result = pipeline(
  '  Hello, World!  ',
  s => s.trim(),          // string → string
  s => s.length             // string → number
);
// result: number(型推論: T=string, U=string, V=number)

注意:型パラメータの数が多いと、型推論が複雑になり、開発者の負担も増えます。4つ以上の型パラメータが必要になった場合は、関数の責務を分割するリファクタリングを検討しましょう。

デフォルト型パラメータ ― 型引数の省略を可能にする

関数の引数にデフォルト値を設定できるように、型パラメータにもデフォルト型を設定できます。= デフォルト型 の構文を使います。

基本構文

デフォルト型パラメータ
// E のデフォルトを Error に設定
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

// E を省略すると Error が使われる
const r1: Result<string> = { ok: false, error: new Error('fail') };

// E を明示的に指定することも可能
type ValidationError = {
  field: string;
  message: string;
};
const r2: Result<string, ValidationError> = {
  ok: false,
  error: { field: 'email', message: 'Invalid email' }
};

制約とデフォルトの組み合わせ

extends(制約)と =(デフォルト)は同時に使えます。デフォルト型は制約を満たす必要があります。

制約 + デフォルト
// T は object に制約、デフォルトは Record<string, unknown>
interface Repository<T extends object = Record<string, unknown>> {
  findAll(): Promise<T[]>;
  findById(id: string): Promise<T | null>;
  save(entity: T): Promise<T>;
}

// 具体的な型を指定して使う
interface User { id: string; name: string; }
const userRepo: Repository<User> = /* ... */;

// デフォルト型を使う(型引数を省略)
const genericRepo: Repository = /* ... */;
// T = Record<string, unknown>

React コンポーネントでの活用

React開発では、コンポーネントのPropsにデフォルト型パラメータを使うパターンがよく見られます。

React での活用例
// ジェネリクスなリストコンポーネント
interface ListProps<T = string> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T = string>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, i) => (
        <li key={keyExtractor(item)}>{renderItem(item, i)}</li>
      ))}
    </ul>
  );
}
ルール 関数のデフォルト引数 デフォルト型パラメータ
必須パラメータの後に配置 デフォルト引数は末尾に置く デフォルト型は末尾に置く
省略可能 呼び出し時に省略するとデフォルト値 型指定時に省略するとデフォルト型
制約との組み合わせ 型注釈で制約 extends= を併用

Conditional Types(条件付き型)と infer

Conditional Types は、型の世界における三項演算子(条件分岐)です。「型 A が型 B のサブタイプなら型 C、そうでなければ型 D」という条件分岐を型レベルで行えます。ジェネリクスと組み合わせると、入力に応じて出力の型を動的に切り替える強力なパターンが実現できます。

基本構文

Conditional Types の基本
// 基本構文: T extends U ? X : Y

// 文字列かどうかを判定する型
type IsString<T> = T extends string ? true : false;

type A = IsString<'hello'>;  // true
type B = IsString<42>;       // false
type C = IsString<string>;  // true

// 配列の要素型を取得する型
type ElementType<T> = T extends (infer U)[] ? U : T;

type D = ElementType<string[]>;   // string
type E = ElementType<number[]>;   // number
type F = ElementType<boolean>;    // boolean(配列でないのでそのまま)

infer キーワード ― 型を抽出する

infer は Conditional Types の extends 節の中でのみ使える特別なキーワードです。「マッチした型の一部を変数のように抽出する」ことができます。

infer による型の抽出
// 関数の戻り値型を取得(ReturnType の実装)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type R1 = MyReturnType<() => string>;     // string
type R2 = MyReturnType<(x: number) => boolean>; // boolean

// 関数の引数型を取得(Parameters の実装)
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;

type P1 = MyParameters<(a: string, b: number) => void>;
// [a: string, b: number]

// Promise の中身を取得
type Awaited<T> = T extends Promise<infer U> ? U : T;

type A1 = Awaited<Promise<string>>;  // string
type A2 = Awaited<number>;           // number(Promiseでないのでそのまま)

分配条件型(Distributive Conditional Types)

Conditional Types にユニオン型を渡すと、ユニオンの各メンバーに対して個別に条件が適用されます。これを分配条件型と呼びます。

分配条件型
type ToArray<T> = T extends any ? T[] : never;

// ユニオンが分配される
type A = ToArray<string | number>;
// = ToArray<string> | ToArray<number>
// = string[] | number[]

// 分配を防ぎたい場合は [] で囲む
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type B = ToArrayNonDist<string | number>;
// = (string | number)[]  ← 分配されない

ポイント:分配条件型は、T裸の型パラメータ(直接 extends の左辺にある)のときだけ発動します。[T] のようにラップすると分配されません。これは ExcludeExtract などのユーティリティ型の基盤になっている重要な仕組みです。

Conditional Types の実践的なパターン

実践パターン
// Exclude: ユニオンから特定の型を除外
type MyExclude<T, U> = T extends U ? never : T;

type E1 = MyExclude<'a' | 'b' | 'c', 'a'>;
// = "b" | "c"

// Extract: ユニオンから特定の型だけを抽出
type MyExtract<T, U> = T extends U ? T : never;

type E2 = MyExtract<string | number | boolean, string | number>;
// = string | number

// NonNullable: null と undefined を除外
type MyNonNullable<T> = T extends null | undefined ? never : T;

type E3 = MyNonNullable<string | null | undefined>;
// = string

// ネストした infer: コンストラクタの引数型を取得
type ConstructorParams<T> = T extends new (...args: infer P) => any ? P : never;

class User {
  constructor(public name: string, public age: number) {}
}

type UserParams = ConstructorParams<typeof User>;
// [name: string, age: number]

infer の位置による抽出パターン一覧

抽出対象 パターン 結果例
配列の要素型 T extends (infer U)[] ? U : T string[]string
関数の戻り値 T extends (...) => infer R ? R : never () => stringstring
関数の引数 T extends (...args: infer P) => any ? P : never (a: string) => void[string]
Promise の中身 T extends Promise<infer U> ? U : T Promise<number>number
コンストラクタ引数 T extends new (...args: infer P) => any ? P : never typeof User[string, number]
インスタンス型 T extends new (...) => infer I ? I : never typeof UserUser

Mapped Types とジェネリクス ― オブジェクト型を動的に変換する

Mapped Types は、既存の型のプロパティを変換・加工して新しい型を生成する仕組みです。{ [K in keyof T]: ... } の構文で、元の型の各プロパティに対して型変換を適用できます。ジェネリクスと組み合わせると、再利用可能な「型変換関数」を作れます。

基本構文

Mapped Types の基本
// すべてのプロパティを readonly にする
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

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

// すべてのプロパティを必須にする
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

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

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

type PartialUser = MyPartial<User>;
// { id?: number; name?: string; email?: string }

type RequiredUser = MyRequired<User>;
// { id: number; name: string; email: string } ← email が必須に

Mapped Types の修飾子

修飾子 意味
readonly 読み取り専用を付加 readonly [K in keyof T]: T[K]
-readonly 読み取り専用を除去 -readonly [K in keyof T]: T[K]
? オプショナルを付加 [K in keyof T]?: T[K]
-? オプショナルを除去 [K in keyof T]-?: T[K]

Key Remapping(as によるキー変換)

TypeScript 4.1 以降、Mapped Types で as を使ってキーを変換できるようになりました。

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

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

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

// 特定の型のプロパティだけをフィルタリング
type StringKeysOnly<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type StringProps = StringKeysOnly<Person>;
// { name: string }  ← age は number なので除外

ポイント:as never にマッピングすると、そのキーは結果から除外されます。これを利用して、特定の条件に合致するプロパティだけを残すフィルタリングが実現できます。

Template Literal Types とジェネリクス ― 文字列リテラル型を操作する

Template Literal Types(テンプレートリテラル型)は、文字列リテラル型をテンプレートのように結合・変換する機能です。TypeScript 4.1 で導入され、ジェネリクスと組み合わせると非常に表現力の高い型定義が可能になります。

基本構文

Template Literal Types の基本
// 基本的な文字列結合
type Greeting = `Hello, ${string}`;

const a: Greeting = 'Hello, World';   // OK
const b: Greeting = 'Hi, World';      // エラー!

// ユニオンとの組み合わせ(直積)
type Color = 'red' | 'blue' | 'green';
type Size = 'sm' | 'md' | 'lg';

type ClassName = `${Color}-${Size}`;
// "red-sm" | "red-md" | "red-lg" | "blue-sm" | ... (9通り)

組み込み文字列操作型

説明
Uppercase<T> 全文字を大文字に Uppercase<"hello">"HELLO"
Lowercase<T> 全文字を小文字に Lowercase<"HELLO">"hello"
Capitalize<T> 先頭を大文字に Capitalize<"hello">"Hello"
Uncapitalize<T> 先頭を小文字に Uncapitalize<"Hello">"hello"

ジェネリクスとの組み合わせ: イベントハンドラー型

イベントハンドラー型の自動生成
// オブジェクトの各プロパティに対して on + 変更イベントを生成
type PropEventHandlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}Change`]: (
    newValue: T[K]
  ) => void;
};

interface FormData {
  name: string;
  age: number;
  active: boolean;
}

type FormHandlers = PropEventHandlers<FormData>;
// {
//   onNameChange: (newValue: string) => void;
//   onAgeChange: (newValue: number) => void;
//   onActiveChange: (newValue: boolean) => void;
// }

Template Literal Types で文字列パターンを解析する

文字列パターン解析
// CamelCase → kebab-case 変換型
type CamelToKebab<S extends string> =
  S extends `${infer Head}${infer Tail}`
    ? Head extends Uppercase<Head>
      ? `-${Lowercase<Head>}${CamelToKebab<Tail>}`
      : `${Head}${CamelToKebab<Tail>}`
    : S;

type R1 = CamelToKebab<'backgroundColor'>;
// "background-color"

type R2 = CamelToKebab<'fontSize'>;
// "font-size"

// パスパラメータの抽出
type ExtractParams<S extends string> =
  S extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    : S extends `${string}:${infer Param}`
      ? Param
      : never;

type Params = ExtractParams<'/users/:userId/posts/:postId'>;
// "userId" | "postId"

ジェネリクスの型推論のしくみ ― いつ明示が必要か

TypeScript は多くの場合、ジェネリクスの型引数を自動的に推論してくれます。しかし、推論がうまくいかないケースもあります。ここでは型推論の仕組みを理解し、明示的な型指定が必要なタイミングを把握しましょう。

推論が成功するケース

型推論が成功する例
// 引数から T を推論
function identity<T>(value: T): T { return value; }
identity(42);  // T = number ← 推論成功

// コールバックの引数型から推論
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
  return arr.map(fn);
}
map([1, 2, 3], n => n.toString()); // T=number, U=string ← 推論成功

// 複数の引数から同じ T を推論
function merge<T>(a: T, b: T): T { return { ...a, ...b }; }
merge({ x: 1 }, { x: 2 }); // T = { x: number } ← 推論成功

推論が期待通りにいかないケース

型推論がうまくいかない例と対処法
// ケース1: 戻り値のみに T がある → 推論できない
function create<T>(): T {
  return {} as T;
}
const user = create<User>(); // 明示的に指定が必要

// ケース2: リテラル型をワイドニングしたくない
function identity<T>(value: T): T { return value; }
const x = identity('hello'); // T = string("hello" ではない)
const y = identity<'hello'>('hello'); // T = "hello" リテラル型

// const 型パラメータ(TS 5.0+)で解決
function identity2<const T>(value: T): T { return value; }
const z = identity2('hello'); // T = "hello" ← リテラル型として推論!

// ケース3: 型パラメータが使われていない位置からの推論
function fetchData<T>(url: string): Promise<T> {
  return fetch(url).then(r => r.json());
}
// url は string → T を推論する手がかりがない
const data = await fetchData<User>('/api/users/1'); // 明示が必要

型推論ルールまとめ

状況 推論 明示の必要性
引数に T が使われている 引数の値から推論 不要
戻り値にのみ T がある 推論不可 必要
リテラル型が必要 ワイドニングされる 明示するか const 修飾子
コールバックの引数 コンテキストから推論 不要
複数箇所から矛盾する推論 共通の親型に拡大 意図と異なれば明示

ポイント:TypeScript 5.0 で導入された const 型パラメータ(<const T>)を使うと、引数をリテラル型として推論させることができます。as const を使う必要がなくなる便利な機能です。

Variadic Tuple Types(可変長タプル型)

TypeScript 4.0 で導入された Variadic Tuple Types は、タプル型にスプレッド演算子(...)を使って可変長の要素を表現する機能です。ジェネリクスと組み合わせると、関数の引数を柔軟に型付けできます。

基本構文

Variadic Tuple Types の基本
// タプルの先頭に要素を追加する型
type Prepend<T, Arr extends unknown[]> = [T, ...Arr];

type A = Prepend<string, [number, boolean]>;
// [string, number, boolean]

// タプルの結合
type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B];

type B = Concat<[string], [number, boolean]>;
// [string, number, boolean]

// 最後の要素を除いたタプル
type Init<T extends unknown[]> =
  T extends [...infer Rest, infer _Last] ? Rest : [];

type C = Init<[string, number, boolean]>;
// [string, number]

実践例: 型安全な関数合成

型安全な bind(部分適用)
// 部分適用を型安全に行う
function partialApply<
  T extends unknown[],
  U extends unknown[],
  R
>(
  fn: (...args: [...T, ...U]) => R,
  ...headArgs: T
): (...tailArgs: U) => R {
  return (...tailArgs) => fn(...headArgs, ...tailArgs);
}

function add(a: number, b: number, c: number): number {
  return a + b + c;
}

const add10 = partialApply(add, 10);
// add10: (b: number, c: number) => number

const add10And20 = partialApply(add, 10, 20);
// add10And20: (c: number) => number

再帰型(Recursive Types)― 複雑なデータ構造の型定義

TypeScriptの型は再帰的に自分自身を参照できます。これにより、ツリー構造やネストしたデータなど、深さが不定なデータ構造を型安全に表現できます。

基本的な再帰型

基本的な再帰型
// JSON型(再帰的定義)
type JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonValue[]           // 再帰: 配列の要素も JsonValue
  | { [key: string]: JsonValue }; // 再帰: オブジェクトの値も JsonValue

// ツリー構造
interface TreeNode<T> {
  value: T;
  children: TreeNode<T>[];  // 再帰: 子ノードも同じ型
}

const tree: TreeNode<string> = {
  value: 'root',
  children: [
    {
      value: 'child1',
      children: [
        { value: 'grandchild1', children: [] }
      ]
    },
    { value: 'child2', children: [] }
  ]
};

ジェネリクスを使った再帰的な型操作

再帰的な型操作
// DeepReadonly: オブジェクトを再帰的に readonly にする
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K];
};

interface Config {
  db: {
    host: string;
    port: number;
    credentials: {
      user: string;
      password: string;
    };
  };
  debug: boolean;
}

type ReadonlyConfig = DeepReadonly<Config>;
// すべてのプロパティ(ネスト含む)が readonly になる

// DeepPartial: 再帰的にオプショナルにする
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object
    ? DeepPartial<T[K]>
    : T[K];
};

// 設定の一部だけ上書きするときに便利
function mergeConfig(base: Config, overrides: DeepPartial<Config>): Config {
  return { ...base, ...overrides } as Config;
}

再帰的なパス型

ネストしたオブジェクトのプロパティパスを型安全に表現する高度なパターンです。

再帰的パス型
// ネストしたプロパティのパスをユニオンとして取得
type Path<T> = T extends object
  ? {
      [K in keyof T & string]:
        | K
        | `${K}.${Path<T[K]>}`;
    }[keyof T & string]
  : never;

type ConfigPaths = Path<Config>;
// "db" | "db.host" | "db.port" | "db.credentials" |
// "db.credentials.user" | "db.credentials.password" | "debug"

注意:再帰型は深くネストすると TypeScript コンパイラの再帰制限(通常50レベル)に引っかかる可能性があります。複雑すぎる再帰型は「Type instantiation is excessively deep and possibly infinite」エラーが出ます。実務では再帰の深さを制限する工夫が必要です。

ユーティリティ型の内部実装を読み解く ― 自作して理解する

TypeScript には PartialPickOmitRecord などの組み込みユーティリティ型が多数用意されています。これらはすべてジェネリクスで実装されており、内部実装を理解することでジェネリクスの応用力が飛躍的に高まります。

Partial<T> ― すべてのプロパティをオプショナルに

Partial の実装
// TypeScript 組み込みの Partial の実装
type Partial<T> = {
  [P in keyof T]?: T[P];
};

// 仕組み:
// 1. keyof T でオブジェクト T のすべてのキーを取得
// 2. [P in ...] で各キーをイテレーション(Mapped Types)
// 3. ? でオプショナル修飾子を付加
// 4. T[P] でそのキーの元の値型を維持

Pick<T, K> ― 特定のプロパティだけを抽出

Pick の実装
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

// 仕組み:
// 1. K extends keyof T → K は T のキーのサブセットに制約
// 2. [P in K] → K に含まれるキーだけをイテレーション
// 3. T[P] → そのキーの値型を維持

// 使用例
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

type UserSummary = Pick<User, 'name' | 'email'>;
// { name: string; email: string }

Omit<T, K> ― 特定のプロパティを除外

Omit の実装
// Omit は Pick + Exclude の組み合わせ
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// 仕組み:
// 1. Exclude<keyof T, K> → T のキーから K を除外
// 2. Pick<T, ...> → 残ったキーだけを抽出

type UserWithoutId = Omit<User, 'id'>;
// { name: string; email: string; age: number }

Record<K, V> ― キーと値の型を指定したオブジェクト

Record の実装
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

// 仕組み:
// 1. K extends keyof any → K は string | number | symbol のサブタイプ
// 2. [P in K] → K の各値をキーとしてイテレーション
// 3. T → すべてのキーに同じ値型を適用

// 使用例: ステータスとメッセージのマッピング
type Status = 'active' | 'inactive' | 'pending';

const statusMessages: Record<Status, string> = {
  active: '有効',
  inactive: '無効',
  pending: '保留中',
};

カスタムユーティリティ型を自作する

組み込みユーティリティ型の仕組みを理解したら、独自のユーティリティ型を自作してみましょう。

カスタムユーティリティ型
// Mutable: readonly を除去
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

// PickByValue: 値の型でフィルタリング
type PickByValue<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K];
};

type StringProps = PickByValue<User, string>;
// { name: string; email: string }

// RequiredKeys: 特定のキーだけを必須にする
type RequiredKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

interface Form {
  name?: string;
  email?: string;
  phone?: string;
}

type NameRequired = RequiredKeys<Form, 'name'>;
// { name: string; email?: string; phone?: string }

// Merge: 2つのオブジェクト型をマージ(後者が優先)
type Merge<A, B> = Omit<A, keyof B> & B;

type Base = { id: number; name: string; version: number };
type Override = { name: string; version: string; extra: boolean };

type Merged = Merge<Base, Override>;
// { id: number; name: string; version: string; extra: boolean }

ユーティリティ型チートシート

ユーティリティ型 用途 内部実装の核
Partial<T> 全プロパティをオプショナルに Mapped Types + ?
Required<T> 全プロパティを必須に Mapped Types + -?
Readonly<T> 全プロパティを読み取り専用に Mapped Types + readonly
Pick<T, K> 特定キーのみ抽出 Mapped Types + K extends keyof T
Omit<T, K> 特定キーを除外 Pick + Exclude
Record<K, V> キーと値の型を指定 Mapped Types
Exclude<T, U> ユニオンから除外 分配条件型
Extract<T, U> ユニオンから抽出 分配条件型
ReturnType<T> 関数の戻り値型を取得 Conditional Types + infer
Parameters<T> 関数の引数型を取得 Conditional Types + infer

実務パターン集 ― ジェネリクスを活かす実践的な設計

ここまで学んだジェネリクスの知識を活用して、実務で頻繁に登場する5つの実践パターンを紹介します。いずれも型安全性と再利用性を両立する設計です。

パターン1: 型安全な API Client

REST API のエンドポイントごとにリクエスト型とレスポンス型を定義し、型安全なクライアントを作ります。

型安全な API Client
// エンドポイントの型定義
interface ApiEndpoints {
  '/users': {
    GET: { response: User[]; params: { page?: number } };
    POST: { response: User; body: Omit<User, 'id'> };
  };
  '/users/:id': {
    GET: { response: User; params: { id: number } };
    PUT: { response: User; body: Partial<User> };
    DELETE: { response: void };
  };
}

// 型安全なクライアント
class ApiClient {
  async get<
    Path extends keyof ApiEndpoints
  >(
    path: Path,
    ...args: 'GET' extends keyof ApiEndpoints[Path]
      ? 'params' extends keyof ApiEndpoints[Path]['GET']
        ? [params: ApiEndpoints[Path]['GET']['params']]
        : []
      : never
  ): Promise<ApiEndpoints[Path]['GET']['response']> {
    // 実装省略
    return {} as any;
  }
}

const api = new ApiClient();

// 戻り値が自動的に型付けされる
const users = await api.get('/users', { page: 1 });
// users: User[] ← 型安全!

const user = await api.get('/users/:id', { id: 1 });
// user: User ← 型安全!

パターン2: 型安全な Event Emitter

イベント名に応じてペイロードの型が自動的に決まるイベントシステムです。

型安全な Event Emitter
// イベントマップの型を定義
interface EventMap {
  login: { userId: string; timestamp: Date };
  logout: { userId: string };
  error: { code: number; message: string };
  click: { x: number; y: number; target: string };
}

class TypedEventEmitter<Events extends Record<string, any>> {
  private listeners = new Map<string, Set<Function>>();

  on<K extends keyof Events & string>(
    event: K,
    listener: (payload: Events[K]) => void
  ): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(listener);
  }

  emit<K extends keyof Events & string>(
    event: K,
    payload: Events[K]
  ): void {
    this.listeners.get(event)?.forEach(fn => fn(payload));
  }
}

const emitter = new TypedEventEmitter<EventMap>();

// payload の型が自動的に推論される
emitter.on('login', (payload) => {
  console.log(payload.userId);  // string ← 型安全
});

emitter.on('error', (payload) => {
  console.log(payload.code);    // number ← 型安全
});

// emit もペイロードの型がチェックされる
emitter.emit('login', { userId: 'u1', timestamp: new Date() }); // OK
emitter.emit('login', { userId: 123 }); // エラー! userId は string

パターン3: 型安全な State Manager

状態管理でよく使われるパターンです。アクションの種類に応じてペイロードの型が決まります。

型安全な State Manager
// 状態の型
interface AppState {
  user: User | null;
  theme: 'light' | 'dark';
  notifications: string[];
}

// Store クラス
class Store<S> {
  private state: S;
  private subscribers = new Set<(state: S) => void>();

  constructor(initialState: S) {
    this.state = initialState;
  }

  getState(): Readonly<S> {
    return this.state;
  }

  // 特定のキーの値だけを更新(型安全)
  update<K extends keyof S>(key: K, value: S[K]): void {
    this.state = { ...this.state, [key]: value };
    this.subscribers.forEach(fn => fn(this.state));
  }

  // 特定のキーの値を購読(型安全)
  select<K extends keyof S>(key: K): S[K] {
    return this.state[key];
  }

  subscribe(fn: (state: S) => void): () => void {
    this.subscribers.add(fn);
    return () => this.subscribers.delete(fn);
  }
}

const store = new Store<AppState>({
  user: null,
  theme: 'light',
  notifications: []
});

store.update('theme', 'dark');       // OK
store.update('theme', 'blue');       // エラー! "blue" は不可
store.update('notifications', ['hello']); // OK

const theme = store.select('theme');    // "light" | "dark"

パターン4: 型安全な Form Validator

型安全な Form Validator
// バリデーションルールの型
type ValidationRule<T> = {
  validate: (value: T) => boolean;
  message: string;
};

// フォーム全体のバリデーションスキーマ
type ValidationSchema<T> = {
  [K in keyof T]?: ValidationRule<T[K]>[];
};

// バリデーションエラーの型
type ValidationErrors<T> = {
  [K in keyof T]?: string[];
};

// バリデーション関数
function validate<T extends Record<string, any>>(
  data: T,
  schema: ValidationSchema<T>
): ValidationErrors<T> {
  const errors: ValidationErrors<T> = {};
  for (const key in schema) {
    const rules = schema[key];
    if (!rules) continue;
    const fieldErrors = rules
      .filter(rule => !rule.validate(data[key]))
      .map(rule => rule.message);
    if (fieldErrors.length > 0) {
      errors[key] = fieldErrors;
    }
  }
  return errors;
}

// 使用例
interface LoginForm {
  email: string;
  password: string;
}

const loginSchema: ValidationSchema<LoginForm> = {
  email: [
    { validate: v => v.length > 0, message: 'メールは必須です' },
    { validate: v => v.includes('@'), message: 'メール形式が不正です' },
  ],
  password: [
    { validate: v => v.length >= 8, message: '8文字以上で入力してください' },
  ],
};

パターン5: 型安全な DI Container

型安全な DI Container
// サービス識別子(シンボルベース)
class ServiceToken<T> {
  private _brand!: T; // phantom type(型情報だけを保持)
  constructor(public readonly name: string) {}
}

// DI Container
class Container {
  private services = new Map<ServiceToken<any>, () => any>();

  // T はトークンの型パラメータから推論される
  register<T>(token: ServiceToken<T>, factory: () => T): void {
    this.services.set(token, factory);
  }

  resolve<T>(token: ServiceToken<T>): T {
    const factory = this.services.get(token);
    if (!factory) throw new Error(`Service not found: ${token.name}`);
    return factory();
  }
}

// 使用例
interface Logger {
  log(message: string): void;
}

interface Database {
  query(sql: string): Promise<any>;
}

// トークンがインターフェースの型を保持
const LOGGER = new ServiceToken<Logger>('Logger');
const DATABASE = new ServiceToken<Database>('Database');

const container = new Container();
container.register(LOGGER, () => ({ log: console.log }));
container.register(DATABASE, () => ({ query: async (sql) => [] }));

const logger = container.resolve(LOGGER);
// logger: Logger ← トークンの型から自動推論!
logger.log('Hello'); // OK

ポイント:DI Container の ServiceToken<T> には _brand というプライベートフィールドがあります。これはPhantom Type(幽霊型)と呼ばれるパターンで、実行時には使われないが、型レベルでの区別を可能にする技法です。

ジェネリクスのアンチパターンと落とし穴

ジェネリクスは強力ですが、使い方を誤ると逆にコードの品質を下げてしまいます。ここでは、実務でよく見かけるアンチパターンと、それを避けるための正しいアプローチを紹介します。

アンチパターン1: 不要なジェネリクス

型パラメータが1箇所でしか使われていない場合、ジェネリクスは不要です。

不要なジェネリクス
// NG: T が引数にしか使われていない(戻り値と関係がない)
function greet<T extends string>(name: T): string {
  return `Hello, ${name}`;
}

// OK: 単に string を受け取れば十分
function greet(name: string): string {
  return `Hello, ${name}`;
}

ジェネリクスが必要かどうかの判定基準

  • 型パラメータが2箇所以上で使われているか(入力と出力の関係を表しているか)
  • 型パラメータが他の型パラメータの制約で使われているか
  • 呼び出し側で型が保持されることに意味があるか

1つでも「はい」があればジェネリクスに意味があります。すべて「いいえ」なら、具体的な型を直接使いましょう。

アンチパターン2: extends any / unknown の無意味な制約

無意味な制約
// NG: T extends any は意味がない(すべての型は any のサブタイプ)
function process<T extends any>(value: T): T { ... }

// OK: 制約が不要ならそのまま
function process<T>(value: T): T { ... }

// 例外: TSX で <T> が JSX と誤認される場合の回避策としては OK
const fn = <T extends unknown>(v: T): T => v;

アンチパターン3: 過度に複雑なジェネリクス

複雑すぎるジェネリクス
// NG: 読めない…
type DeepMerge<
  A extends Record<string, any>,
  B extends Record<string, any>,
  Keys extends keyof A & keyof B = keyof A & keyof B,
  OnlyA extends keyof A = Exclude<keyof A, keyof B>,
  OnlyB extends keyof B = Exclude<keyof B, keyof A>
> = { ... };

// OK: ヘルパー型に分割して可読性を向上
type CommonKeys<A, B> = keyof A & keyof B;
type UniqueKeys<A, B> = Exclude<keyof A, keyof B>;

type DeepMerge<
  A extends Record<string, any>,
  B extends Record<string, any>
> = { /* CommonKeys と UniqueKeys を使って実装 */ };

アンチパターン4: as 型アサーションでジェネリクスをバイパス

型アサーションの乱用
// NG: as で型安全性を放棄している
function parse<T>(json: string): T {
  return JSON.parse(json) as T; // 実行時には何もチェックされない!
}

// OK: ランタイムバリデーションと組み合わせる
function safeParse<T>(
  json: string,
  validator: (data: unknown) => data is T
): T | null {
  const data = JSON.parse(json);
  return validator(data) ? data : null;
}

アンチパターン5: 型パラメータの命名が不適切

不適切な命名
// NG: 意味不明な名前
function process<A, B, C, D>(
  config: A,
  transform: (v: B) => C,
  fallback: D
): C | D { ... }

// OK: 役割がわかる名前
function process<
  TConfig,
  TInput,
  TOutput,
  TFallback
>(
  config: TConfig,
  transform: (v: TInput) => TOutput,
  fallback: TFallback
): TOutput | TFallback { ... }

アンチパターンまとめ

アンチパターン 問題点 改善方法
不要なジェネリクス コードが複雑になるだけ 具体的な型を直接使う
extends any 制約が機能しない 制約を外すか、意味のある制約にする
過度な複雑さ 読めない・保守できない ヘルパー型に分割する
as による型バイパス 実行時の安全性がない ランタイムバリデーションを併用
不適切な命名 可読性が低下 T+意味のある接尾辞を使う

よくあるエラーメッセージと対処法

ジェネリクスを使っていると、TypeScript 独特のエラーメッセージに出会うことがあります。主要なエラーとその解決方法を紹介します。

エラー1: Type ‘X’ is not assignable to type ‘T’

型の代入エラー
// エラー: Type 'default' is not assignable to type 'T'
function getOrDefault<T>(value: T | undefined): T {
  return value ?? 'default'; // エラー!
}

// 原因: T は何でもあり得る(numberかもしれない)ので、
// string の "default" を T として返すのは型安全ではない

// 解決策1: デフォルト値も T 型として受け取る
function getOrDefault<T>(value: T | undefined, defaultValue: T): T {
  return value ?? defaultValue;
}

エラー2: Type ‘T’ does not satisfy the constraint

制約エラー
// エラー: Type 'T' does not satisfy the constraint 'string'
function processKey<T>(obj: Record<T, any>) {} // エラー!

// 原因: Record の K は string | number | symbol に制約されている
// T は制約なしなので、boolean なども含まれうる

// 解決策: T に制約を追加
function processKey<T extends string>(obj: Record<T, any>) {} // OK

エラー3: Property ‘X’ does not exist on type ‘T’

プロパティアクセスエラー
// エラー: Property 'length' does not exist on type 'T'
function getLength<T>(value: T): number {
  return value.length; // エラー! T に length があるとは限らない
}

// 解決策: length を持つ型に制約する
function getLength<T extends { length: number }>(value: T): number {
  return value.length; // OK
}

エラー4: Type instantiation is excessively deep and possibly infinite

再帰制限エラー
// エラー: 再帰が深すぎる
type InfiniteRecursion<T> = {
  value: T;
  next: InfiniteRecursion<T[]>; // 毎回 T[] が深くなり無限ループ
};

// 解決策: 再帰の深さを制限するカウンターを導入
type MaxDepth = [never, 0, 1, 2, 3, 4, 5];

type DeepReadonly<T, Depth extends number = 5> =
  Depth extends 0
    ? T
    : {
        readonly [K in keyof T]: T[K] extends object
          ? DeepReadonly<T[K], MaxDepth[Depth]>
          : T[K];
      };

エラーメッセージ早見表

エラーメッセージ 原因 対処法
not assignable to type ‘T’ T に具体的な値を代入 引数でデフォルト値を受け取る
does not satisfy the constraint T が制約を満たさない extends で適切な制約を追加
Property does not exist on type ‘T’ T にプロパティがない extends で必要なプロパティの制約を追加
excessively deep and possibly infinite 再帰型の無限ループ 再帰の深さを制限する
Argument not assignable to parameter 呼び出し時の型不一致 型引数を明示的に指定する

まとめ

この記事では、TypeScriptのジェネリクスを基礎から実務レベルまで体系的に解説しました。主要なポイントを振り返りましょう。

トピック キーポイント
基本概念 ジェネリクスは型をパラメータ化し、any と違って型安全性を保つ
基本構文 関数・インターフェース・クラス・型エイリアスで利用可能
型制約(extends) 型パラメータに上限境界を設定して安全にプロパティアクセス
keyof + T[K] オブジェクトのプロパティに型安全にアクセスする基本パターン
Conditional Types 型レベルの条件分岐。infer で型を抽出し、分配で一括処理
Mapped Types 既存の型を変換。as によるキーリマッピングが強力
Template Literal Types 文字列リテラル型の結合・変換。イベント名の自動生成など
再帰型 ネストしたデータ構造の型定義。深さ制限に注意
ユーティリティ型 Partial, Pick, Omit, Record はすべてジェネリクスで実装されている
実務パターン API Client, Event Emitter, Store, Validator, DI Container

ジェネリクスは TypeScript の型システムの中核を成す機能です。最初は「<T> って何?」という段階から始まりますが、extends による制約、keyof との組み合わせ、Conditional Types の infer、Mapped Types の as リマッピングと理解を深めるごとに、型で表現できることの幅が飛躍的に広がります。

ポイントは、「型が必要な場面で正しいレベルのジェネリクスを使う」ことです。不要な箇所での乱用は避け、入出力の型の関係を表現する場面で積極的に活用していきましょう。