DevToolBox無料
ブログ

TypeScript ジェネリクス完全解説:実践ガイド

14分by DevToolBox

TypeScript のジェネリクスは型システムで最も強力な機能の一つです。型をパラメータ化することで、柔軟で再利用可能かつ型安全なコードを書くことができます。ジェネリクスを使えば、すべてのデータ型に対して関数やクラスを複製する必要がなく、一度書けば完全な型安全性を維持しながら任意の型で使用できます。このガイドでは基礎から高度なパターンまですべてカバーします。

最初のジェネリック関数

典型的な出発点は恒等関数です。受け取ったものをそのまま返す関数です。ジェネリクスなしでは、any を使って型情報を失うか、各型に別々の関数を書く必要があります。

ジェネリクスでは、プレースホルダーとして機能する型パラメータ(慣例的に T と命名)を定義します。関数を呼び出すと、TypeScript が実際の型を自動的に推論します。

// Without generics — loses type info
function identityAny(value: any): any {
  return value;
}
const result1 = identityAny("hello"); // type: any (not string!)

// With generics — preserves type info
function identity<T>(value: T): T {
  return value;
}
const result2 = identity("hello");    // type: string ✓
const result3 = identity(42);         // type: number ✓
const result4 = identity(true);       // type: boolean ✓

// Explicit type argument (rarely needed)
const result5 = identity<string>("hello"); // type: string

ジェネリック関数

ジェネリック関数は複数の型パラメータを受け取り、デフォルト型を持つこともできます。これにより、ユーティリティ関数の構築において非常に汎用的になります。

// Multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}
const p = pair("hello", 42); // type: [string, number]

// Swap function with two type params
function swap<A, B>(tuple: [A, B]): [B, A] {
  return [tuple[1], tuple[0]];
}
const swapped = swap(["hello", 42]); // type: [number, string]

// Default type parameters
function createArray<T = string>(length: number, value: T): T[] {
  return Array(length).fill(value);
}
const strings = createArray(3, "x");   // T inferred as string
const numbers = createArray(3, 0);     // T inferred as number
const defaults = createArray<>(3, ""); // T defaults to string

デフォルト型パラメータはデフォルト関数パラメータと同じように機能します。呼び出し側が型を指定しない場合、デフォルトが使用されます。

ジェネリックインターフェースと型

ジェネリクスは関数に限定されません。インターフェース型エイリアスをパラメータ化して、任意の型で動作する柔軟なデータ構造を作成できます。

// Generic interface
interface Box<T> {
  value: T;
  label: string;
}

const stringBox: Box<string> = { value: "hello", label: "greeting" };
const numberBox: Box<number> = { value: 42, label: "answer" };

// Generic interface with multiple params
interface KeyValuePair<K, V> {
  key: K;
  value: V;
}

// Generic type alias
type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

const ok: Result<string> = { success: true, data: "hello" };
const fail: Result<string> = { success: false, error: new Error("oops") };

// Generic type for callbacks
type AsyncCallback<T> = (error: Error | null, data: T | null) => void;

// Generic type for nullable values
type Nullable<T> = T | null | undefined;

ジェネリックインターフェースは API レスポンス、コレクション、データコンテナの形状を定義する際に特に便利です。

ジェネリッククラス

クラスもジェネリックにできます。スタック、キュー、リンクリストなどのデータ構造や、複数の型で動作する必要があるコンテナの構築に便利です。

// Generic Stack class
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;
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.pop(); // type: number | undefined

const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.peek(); // type: string | undefined

// Generic class with multiple type params
class HashMap<K, V> {
  private map = new Map<K, V>();

  set(key: K, value: V): void {
    this.map.set(key, value);
  }

  get(key: K): V | undefined {
    return this.map.get(key);
  }

  has(key: K): boolean {
    return this.map.has(key);
  }
}

型パラメータはクラス全体で利用可能です。プロパティ、メソッドパラメータ、戻り値の型で使えます。

extends によるジェネリック制約

制約のないジェネリクスはあらゆる型を受け入れますが、許可される型を絞り込む必要がある場合があります。extends キーワードで制約を追加できます。

keyof によるオブジェクトキーの制約

非常に一般的なパターンは、型パラメータを別の型の有効なキーに制約することです。これにより型安全なプロパティアクセスが保証されます。

// getProperty with keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Alice", age: 30, email: "alice@example.com" };

getProperty(user, "name");  // type: string ✓
getProperty(user, "age");   // type: number ✓
// getProperty(user, "foo"); // Error: "foo" is not assignable to "name" | "age" | "email"

// Pick specific keys from an object
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 subset = pick(user, ["name", "email"]);
// type: { name: string; email: string }

特定の形状への制約

インターフェースやオブジェクト型を拡張することで、型パラメータに特定のプロパティを要求できます。

// Constraint to objects with a length property
interface HasLength {
  length: number;
}

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

logLength("hello");     // OK — string has length
logLength([1, 2, 3]);   // OK — array has length
logLength({ length: 5, name: "test" }); // OK — has length
// logLength(42);       // Error — number has no length property

// Constraint to objects with an id
interface HasId {
  id: number | string;
}

function findById<T extends HasId>(items: T[], id: T["id"]): T | undefined {
  return items.find(item => item.id === id);
}

const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
];

const found = findById(users, 1);
// type: { id: number; name: string } | undefined

条件型

条件型は T extends U ? X : Y 構文を使用して、条件に基づいて2つの型から選択できます。多くの高度な型レベルの計算の基盤です。

// Basic conditional type
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false
type C = IsString<"hello">; // true

// Flatten arrays, leave other types alone
type Flatten<T> = T extends Array<infer Item> ? Item : T;

type D = Flatten<string[]>;    // string
type E = Flatten<number[][]>;  // number[]
type F = Flatten<string>;      // string (not an array, returned as-is)

// Extract return type (simplified ReturnType)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type G = MyReturnType<() => string>;           // string
type H = MyReturnType<(x: number) => boolean>; // boolean

// Extract promise value
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type I = UnwrapPromise<Promise<string>>; // string
type J = UnwrapPromise<number>;          // number

条件型は infer と組み合わせると特に強力になります。infer を使うと他の型の中から型を抽出できます。

マップ型

マップ型は既存の型のプロパティを変換して新しい型を作成します。[K in keyof T] 構文でキーを反復します。

// Make all properties optional (like Partial<T>)
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Make all properties required (like Required<T>)
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

// Make all properties readonly (like Readonly<T>)
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Make all properties nullable
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

// Transform property types
type Stringify<T> = {
  [K in keyof T]: string;
};

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

type StringifiedUser = Stringify<User>;
// { name: string; age: string; active: string }

// Mapped type with conditional
type OptionalNullable<T> = {
  [K in keyof T]: undefined extends T[K] ? T[K] | null : T[K];
};

マップ型では readonly?(オプション)などの修飾子を追加・削除できます。

組み込みユーティリティ型

TypeScript にはジェネリクスで構築された多くのユーティリティ型が付属しています。最もよく使用されるものを紹介します:

ユーティリティ型説明
Partial<T>全プロパティをオプションにPartial<User>
Required<T>全プロパティを必須にRequired<Partial<User>>
Pick<T, K>指定プロパティを選択Pick<User, "name" | "email">
Omit<T, K>指定プロパティを除外Omit<User, "password">
Record<K, V>キーと値の型マッピングを作成Record<string, number>
Readonly<T>全プロパティを読み取り専用にReadonly<Config>
ReturnType<T>関数の戻り値の型を抽出ReturnType<typeof fn>
Parameters<T>関数パラメータ型のタプルを抽出Parameters<typeof fn>
// Utility types in action
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// For update forms — all fields optional
type UpdateUser = Partial<User>;

// Public user — omit password
type PublicUser = Omit<User, "password">;

// User creation — omit auto-generated fields
type CreateUser = Omit<User, "id" | "createdAt">;

// Lookup table
type UserLookup = Record<number, User>;

// Immutable user from API
type FrozenUser = Readonly<User>;

// Extract function types
function fetchUser(id: number): Promise<User> { /* ... */ }
type FetchReturn = ReturnType<typeof fetchUser>;     // Promise<User>
type FetchParams = Parameters<typeof fetchUser>;     // [number]

実践的なパターン

ジェネリック API レスポンスラッパー

ジェネリクスの最も一般的な使用法の一つは、API レスポンスを一貫した構造でラップすることです。各エンドポイントが特定のデータ型を保持しながら同じ形式でデータを返すことを保証します。

// Generic API response wrapper
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: string;
}

interface PaginatedResponse<T> extends ApiResponse<T[]> {
  pagination: {
    page: number;
    pageSize: number;
    totalItems: number;
    totalPages: number;
  };
}

// Usage with specific types
interface User {
  id: number;
  name: string;
  email: string;
}

interface Product {
  id: number;
  title: string;
  price: number;
}

async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
  const res = await fetch(url);
  return res.json();
}

// TypeScript knows the exact data type
const userRes = await fetchApi<User>("/api/users/1");
console.log(userRes.data.name); // type: string ✓

const productsRes = await fetchApi<Product[]>("/api/products");
console.log(productsRes.data[0].price); // type: number ✓

ジェネリック React コンポーネント Props

ジェネリクスを使えば、props の型安全性を維持しながら、異なるデータ型で動作する React コンポーネントを構築できます。

// Generic list component props
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
  onItemClick?: (item: T) => void;
  emptyMessage?: string;
}

function GenericList<T>({
  items,
  renderItem,
  keyExtractor,
  onItemClick,
  emptyMessage = "No items",
}: ListProps<T>) {
  if (items.length === 0) return <p>{emptyMessage}</p>;
  return (
    <ul>
      {items.map((item, index) => (
        <li
          key={keyExtractor(item)}
          onClick={() => onItemClick?.(item)}
        >
          {renderItem(item, index)}
        </li>
      ))}
    </ul>
  );
}

// Usage — TypeScript infers T from items
<GenericList
  items={users}
  keyExtractor={(user) => user.id}
  renderItem={(user) => <span>{user.name}</span>}
  onItemClick={(user) => console.log(user.email)}
/>

ジェネリックリポジトリパターン

リポジトリパターンはジェネリックインターフェースの背後にデータアクセスを抽象化します。ビジネスロジックを変更せずにデータソースを交換できます。

// Generic repository interface
interface Entity {
  id: number | string;
}

interface Repository<T extends Entity> {
  findAll(): Promise<T[]>;
  findById(id: T["id"]): Promise<T | null>;
  create(data: Omit<T, "id">): Promise<T>;
  update(id: T["id"], data: Partial<T>): Promise<T>;
  delete(id: T["id"]): Promise<boolean>;
}

// Concrete implementation
class ApiRepository<T extends Entity> implements Repository<T> {
  constructor(private baseUrl: string) {}

  async findAll(): Promise<T[]> {
    const res = await fetch(this.baseUrl);
    return res.json();
  }

  async findById(id: T["id"]): Promise<T | null> {
    const res = await fetch(`${this.baseUrl}/${id}`);
    if (!res.ok) return null;
    return res.json();
  }

  async create(data: Omit<T, "id">): Promise<T> {
    const res = await fetch(this.baseUrl, {
      method: "POST",
      body: JSON.stringify(data),
    });
    return res.json();
  }

  async update(id: T["id"], data: Partial<T>): Promise<T> {
    const res = await fetch(`${this.baseUrl}/${id}`, {
      method: "PATCH",
      body: JSON.stringify(data),
    });
    return res.json();
  }

  async delete(id: T["id"]): Promise<boolean> {
    const res = await fetch(`${this.baseUrl}/${id}`, { method: "DELETE" });
    return res.ok;
  }
}

// Usage
interface User extends Entity { id: number; name: string; email: string; }
const userRepo = new ApiRepository<User>("/api/users");
const users = await userRepo.findAll(); // type: User[]

ジェネリックイベントエミッター

型安全なイベントエミッターにより、イベント名とペイロードが正しく型付けされます。未知のデータを持つ文字列ベースのイベントは不要になります。

// Type-safe event emitter
type EventMap = Record<string, any>;
type EventCallback<T> = (payload: T) => void;

class TypedEmitter<Events extends EventMap> {
  private listeners: {
    [K in keyof Events]?: EventCallback<Events[K]>[];
  } = {};

  on<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(callback);
  }

  off<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
    const cbs = this.listeners[event];
    if (cbs) {
      this.listeners[event] = cbs.filter(cb => cb !== callback);
    }
  }

  emit<K extends keyof Events>(event: K, payload: Events[K]): void {
    const cbs = this.listeners[event];
    if (cbs) {
      cbs.forEach(cb => cb(payload));
    }
  }
}

// Define your event types
interface AppEvents {
  "user:login": { userId: string; timestamp: number };
  "user:logout": { userId: string };
  "item:added": { itemId: string; quantity: number };
  "error": { message: string; code: number };
}

const emitter = new TypedEmitter<AppEvents>();

// Fully typed — payload type is inferred
emitter.on("user:login", (payload) => {
  console.log(payload.userId);    // type: string ✓
  console.log(payload.timestamp); // type: number ✓
});

// Error: "unknown:event" is not assignable to keyof AppEvents
// emitter.on("unknown:event", () => {});

よくある間違い

開発者がジェネリクスで犯す最も頻繁な間違いとその修正方法:

間違い問題修正
ジェネリクスの代わりに any を使用すべての型情報が失われるany の代わりに型パラメータ T を使用
型パラメータの過度な制約有効な型でジェネリクスが使えなくなる実際に必要なものだけを制約する
不要な型パラメータ一度しか使わない T を追加すると複雑さが増すT が戻り値の型にのみ現れる場合は具体的な型を使用
プロパティアクセス時に制約を使用しないTypeScript エラー:型 T にプロパティが存在しないextends 制約を追加:T extends { prop: type }
ジェネリックのデフォルトと制約の混同デフォルト型は制限しない。呼び出し側は他の型を渡せる制限には extends を、デフォルトには = を使用
可能な場合に推論を使わないTS が推論できるのに明示的に型を渡している可能な限り TypeScript に引数から型を推論させる
readonly T[] の代わりに T[] を使用読み取り専用であるべき配列の変更を許可してしまうAPI からの不変データには readonly T[] を使用
// ❌ Mistake: Using any
function badParse(json: string): any {
  return JSON.parse(json);
}
const data = badParse('{"name":"Alice"}');
data.whatever; // No error — but might crash at runtime!

// ✓ Correction: Using generics with validation
function safeParse<T>(json: string, validator: (data: unknown) => data is T): T | null {
  try {
    const parsed = JSON.parse(json);
    return validator(parsed) ? parsed : null;
  } catch {
    return null;
  }
}

// ❌ Mistake: Unnecessary type parameter
function badLength<T>(arr: T[]): number {
  return arr.length; // T is never meaningfully used
}

// ✓ Correction: Just use the concrete type
function goodLength(arr: unknown[]): number {
  return arr.length;
}

// ❌ Mistake: Missing constraint
function badGetName<T>(obj: T): string {
  return obj.name; // Error: Property 'name' does not exist on type 'T'
}

// ✓ Correction: Add constraint
function goodGetName<T extends { name: string }>(obj: T): string {
  return obj.name; // OK ✓
}

よくある質問

ジェネリクスと any 型の違いは何ですか?

ジェネリクスは関数やクラス全体で型情報を保持しますが、any はそれを破棄します。ジェネリクスでは文字列を渡すと、TypeScript は戻り値の型も文字列であることを知っています。any ではすべての型情報が失われます。

ジェネリクスと関数オーバーロード、いつどちらを使うべきですか?

すべての型でロジックが同じで型の関係を保持するだけの場合はジェネリクスを使用します。入力型によって実装が異なる場合や、特定の入力型を特定の出力型にマッピングする必要がある場合はオーバーロードを使用します。

TSX ファイルでアロー関数にジェネリクスを使えますか?

はい。ただし JSX の曖昧さを避けるためにトレイリングカンマを追加する必要があります:const fn = <T,>(arg: T): T => arg。カンマがパーサーにこれが JSX タグではなくジェネリクスであることを伝えます。

ジェネリクスの型パラメータはいくつにすべきですか?

できるだけ少なく。1~2個が一般的です。3個が必要な場合もあります。3個以上必要な場合はリファクタリングを検討してください。各型パラメータには明確な目的が必要です。

ジェネリクスは実行時のパフォーマンスに影響しますか?

いいえ。ジェネリクスは純粋にコンパイル時の機能です。コンパイル中に完全に消去され、実行時のオーバーヘッドは発生しません。生成された JavaScript にはジェネリック型パラメータの痕跡はありません。

型パラメータの命名規則は何ですか?

単一の大文字が慣例です:T は型、K はキー、V は値、E は要素、R は戻り値の型。複数パラメータの複雑なジェネリクスでは TData、TError などの説明的な名前も許容され、可読性が向上します。

𝕏 Twitterin LinkedIn
この記事は役に立ちましたか?

最新情報を受け取る

毎週の開発ヒントと新ツール情報。

スパムなし。いつでも解除可能。

Try These Related Tools

TSJSON to TypeScriptJSTypeScript to JavaScriptGTGraphQL to TypeScript

Related Articles

JSON から TypeScript へ:実例付き完全ガイド

JSON データを TypeScript インターフェースに自動変換する方法を学びます。ネストされたオブジェクト、配列、オプショナルフィールド、ベストプラクティスを網羅。

TypeScript vs JavaScript:いつ、どうやって変換するか

TypeScript を JavaScript に変換すべきタイミングとその逆について実践的に解説。移行戦略、ツール、バンドルサイズへの影響、チームの考慮事項。

JSON to Zod Schema:TypeScriptでの型安全なランタイムバリデーション

JSONをZodスキーマに変換して、TypeScriptで型安全なランタイムバリデーションを実現する方法を学びます。

GraphQL から TypeScript: コード生成ガイド

GraphQL スキーマから TypeScript 型を自動生成。