Chapter 06

ジェネリクス

型をパラメータ化して再利用可能なコードを書く方法を学びます

35 min

このチャプターで学ぶこと

  • ジェネリクスの基本概念を理解できる
  • ジェネリック関数を作成できる
  • 制約(constraints)で型パラメータを制限できる
  • ユーティリティ型(Partial, Pick, Omit, Record)を使える

ジェネリクス

前のチャプターまでで、基本的な型注釈やインターフェースの使い方を学びました。しかし、型を使い始めると「同じロジックなのに型だけが違う関数」を何度も書くことになりがちです。

たとえば、配列の最初の要素を取得する関数を考えてみましょう。

// 文字列配列用
function getFirstString(arr: string[]): string | undefined {
  return arr[0];
}

// 数値配列用
function getFirstNumber(arr: number[]): number | undefined {
  return arr[0];
}

// ユーザー配列用
function getFirstUser(arr: User[]): User | undefined {
  return arr[0];
}

ロジックは完全に同じなのに、型が違うだけで3つの関数を書いています。新しい型が増えるたびに関数も増えてしまいます。

この問題を解決するのが ジェネリクス(Generics) です。

ジェネリクスとは「型のパラメータ」

ジェネリクスを一言でいうと、関数のパラメータと同じ仕組みを、型に対して適用するものです。

// 普通の関数:値をパラメータ化
function greet(name: string): string {
  return `こんにちは、${name}さん!`;
}
greet("太郎"); // 呼び出し時に値を渡す

// ジェネリック関数:型をパラメータ化
function getFirst<T>(arr: T[]): T | undefined {
  return arr[0];
}
getFirst<string>(["a", "b", "c"]); // 呼び出し時に型を渡す

<T> の部分が 型パラメータ です。関数を呼ぶときに具体的な型を渡すことで、さまざまな型に対応できます。

📝 Laravelで例えると

Laravelのコレクション Collection は、PHPDocで @template T を使ってジェネリクスを表現しています。Collection<User> のように型パラメータを指定すると、first() の戻り値が User 型になりますよね。TypeScriptのジェネリクスはこれと同じ発想ですが、PHPDocではなく言語レベルで完全にサポートされています。

ジェネリック関数の基本

identity 関数で理解する

ジェネリクスの定番例として、受け取った値をそのまま返す identity 関数があります。

// ジェネリック関数の定義
function identity<T>(arg: T): T {
  return arg;
}

// 使い方1:型パラメータを明示する
const result1 = identity<string>("こんにちは");  // result1: string
const result2 = identity<number>(42);             // result2: number

// 使い方2:TypeScript に型を推論させる(こちらが一般的)
const result3 = identity("こんにちは");  // result3: string(自動推論)
const result4 = identity(42);             // result4: number(自動推論)

TypeScriptは渡された引数から型パラメータ T を推論してくれるので、多くの場合は <string> のような明示的な指定は不要です。

先ほどの問題を解決する

冒頭の「配列の最初の要素を取得する関数」をジェネリクスで書き直しましょう。

// 1つの関数であらゆる型に対応
function getFirst<T>(arr: T[]): T | undefined {
  return arr[0];
}

// 使い方
const firstString = getFirst(["React", "Vue", "Angular"]); // string | undefined
const firstNumber = getFirst([1, 2, 3]);                     // number | undefined

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

const users: User[] = [
  { id: 1, name: "山田太郎" },
  { id: 2, name: "田中花子" },
];
const firstUser = getFirst(users); // User | undefined

3つの関数が1つになりました。しかも型安全です。

💡 ヒント

型パラメータの名前は何でもよいですが、慣習として以下がよく使われます。

  • T — Type(一般的な型)
  • U, V — 2番目、3番目の型パラメータ
  • K — Key(オブジェクトのキー)
  • V — Value(オブジェクトの値)
  • E — Element(要素)

複数の型パラメータ

型パラメータはカンマ区切りで複数指定できます。

// 2つの値をペアにする関数
function makePair<T, U>(first: T, second: U): { first: T; second: U } {
  return { first, second };
}

const pair1 = makePair("名前", 25);
// 型: { first: string; second: number }

const pair2 = makePair(1, true);
// 型: { first: number; second: boolean }

もう少し実用的な例として、オブジェクトからキーを指定して値を取り出す関数を作ってみましょう。

// オブジェクトとキーを受け取り、対応する値を返す
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: "山田太郎", age: 30 };

const name = getProperty(user, "name");  // string
const age = getProperty(user, "age");    // number

// コンパイルエラー!"email" は user のキーに存在しない
// const email = getProperty(user, "email");

K extends keyof T によって、2番目の引数には T のキー名しか渡せないよう制限しています。存在しないキーを指定するとコンパイルエラーになるため、タイポを防げます。

制約(constraints)で型パラメータを制限する

ジェネリクスはどんな型でも受け取れますが、場合によっては「特定のプロパティを持つ型だけ受け付けたい」ことがあります。

// この関数は .length プロパティにアクセスしたい
function logLength<T>(arg: T): void {
  // エラー!T に .length があるかわからない
  // console.log(arg.length);
}

T はどんな型でもよいので、.length プロパティがある保証がありません。こんなときに 制約(constraints) を使います。

// .length を持つ型だけに制限する
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(arg: T): void {
  // OK!T は必ず .length を持つ
  console.log(`長さ: ${arg.length}`);
}

logLength("Hello");       // OK:文字列は .length を持つ
logLength([1, 2, 3]);     // OK:配列は .length を持つ
logLength({ length: 10 }); // OK:.length プロパティがある

// エラー!number は .length を持たない
// logLength(42);

<T extends HasLength> は「THasLength インターフェースを満たす型でなければならない」という意味です。

📝 Railsで例えると

Rubyでは respond_to?(:length) でメソッドの存在を実行時にチェックしますが、TypeScriptの制約はコンパイル時にチェックするため、実行前にエラーを発見できます。ダックタイピングの考え方は似ていますが、チェックのタイミングが違います。

実用例:APIレスポンスの処理

バックエンド開発でよくある、APIレスポンスを処理する関数を作ってみましょう。

// APIレスポンスは必ず data プロパティを持つ
interface ApiResponse {
  data: unknown;
  status: number;
}

// data プロパティを持つオブジェクトに制限
function extractData<T extends { data: unknown }>(response: T): T["data"] {
  return response.data;
}

// さまざまなレスポンス型で使える
interface UserResponse {
  data: { id: number; name: string };
  status: number;
  headers: Record<string, string>;
}

interface ProductResponse {
  data: { id: number; title: string; price: number };
  status: number;
}

const userRes: UserResponse = {
  data: { id: 1, name: "山田太郎" },
  status: 200,
  headers: { "content-type": "application/json" },
};

const userData = extractData(userRes);
// 型: { id: number; name: string }

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

関数だけでなく、インターフェースや型エイリアスにもジェネリクスを使えます。

// ジェネリックインターフェース
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

// 使う側で具体的な型を指定する
const userResponse: ApiResponse<{ id: number; name: string }> = {
  data: { id: 1, name: "山田太郎" },
  status: 200,
  message: "成功",
};

const productResponse: ApiResponse<{ id: number; title: string; price: number }> = {
  data: { id: 1, title: "TypeScript入門", price: 3000 },
  status: 200,
  message: "成功",
};
// 型エイリアスでもジェネリクスが使える
type Result<T> = {
  success: true;
  data: T;
} | {
  success: false;
  error: string;
};

// 使い方
function fetchUser(id: number): Result<{ id: number; name: string }> {
  if (id <= 0) {
    return { success: false, error: "無効なIDです" };
  }
  return { success: true, data: { id, name: "山田太郎" } };
}

const result = fetchUser(1);
if (result.success) {
  // ここでは result.data にアクセスできる
  console.log(result.data.name);
} else {
  // ここでは result.error にアクセスできる
  console.log(result.error);
}
💡 ヒント

Result<T> のようなパターンは、RustやElmの Result 型に似ています。成功と失敗を型で表現することで、エラー処理を漏れなく行えます。

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

TypeScriptには、よく使うジェネリックな型変換がユーティリティ型として組み込まれています。これらを知っているとコードが格段にシンプルになります。

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

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

// Partial<User> は全プロパティがオプショナルになる
type PartialUser = Partial<User>;
// {
//   id?: number;
//   name?: string;
//   email?: string;
//   age?: number;
// }

// 更新関数で便利:変更したいフィールドだけ渡せる
function updateUser(id: number, updates: Partial<User>): void {
  // updates は全プロパティがオプショナル
  console.log(`ユーザー ${id} を更新:`, updates);
}

updateUser(1, { name: "新しい名前" });          // name だけ更新
updateUser(2, { email: "new@example.com", age: 31 }); // 複数フィールドを更新
📝 Laravelで例えると

Partial<User> は、Laravel の $request->only(['name', 'email']) で一部フィールドだけ更新するパターンに近いです。「全フィールドを渡す必要はないが、渡すなら正しい型でなければならない」という制約を表現できます。

Required<T> — すべてのプロパティを必須に

Partial<T> の逆で、すべてのプロパティを必須にします。

interface Config {
  host?: string;
  port?: number;
  debug?: boolean;
}

// Required<Config> はすべて必須になる
type FullConfig = Required<Config>;
// {
//   host: string;
//   port: number;
//   debug: boolean;
// }

// デフォルト値を埋めた後の設定として使える
function createConfig(partial: Config): Required<Config> {
  return {
    host: partial.host ?? "localhost",
    port: partial.port ?? 3000,
    debug: partial.debug ?? false,
  };
}

Pick<T, K> — 特定のプロパティだけ取り出す

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  createdAt: Date;
}

// name と email だけ取り出す
type UserSummary = Pick<User, "name" | "email">;
// {
//   name: string;
//   email: string;
// }

// 一覧表示用の型を作る
function renderUserList(users: UserSummary[]): void {
  users.forEach(user => {
    console.log(`${user.name} (${user.email})`);
  });
}

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

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

// パスワードハッシュを除外した公開用の型
type PublicUser = Omit<User, "passwordHash">;
// {
//   id: number;
//   name: string;
//   email: string;
// }

// 新規作成時は id がまだない
type CreateUserInput = Omit<User, "id" | "passwordHash">;
// {
//   name: string;
//   email: string;
// }
💡 ヒント

Pick は「使うものを選ぶ」、Omit は「使わないものを除く」です。プロパティが少ない場合は Pick、多い場合は Omit を使うとシンプルに書けます。

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

// 曜日をキー、予定を値とするオブジェクト
type Schedule = Record<string, string[]>;

const weeklySchedule: Schedule = {
  月曜日: ["朝会", "開発"],
  火曜日: ["コードレビュー", "設計"],
  水曜日: ["スプリントプランニング"],
};

// ステータスごとの件数
type StatusCount = Record<"todo" | "doing" | "done", number>;

const counts: StatusCount = {
  todo: 5,
  doing: 3,
  done: 12,
};

Readonly<T>ReadonlyArray<T> — 変更不可にする

interface Config {
  apiUrl: string;
  timeout: number;
}

// すべてのプロパティが readonly になる
const config: Readonly<Config> = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
};

// エラー!readonly プロパティに代入できない
// config.apiUrl = "https://other.com";

// 配列も変更不可にできる
const numbers: ReadonlyArray<number> = [1, 2, 3];

// エラー!push, pop, splice などが使えない
// numbers.push(4);

// 読み取りは OK
console.log(numbers[0]); // 1
console.log(numbers.length); // 3
⚠️ 注意

Readonly<T>シャロー(浅い) です。ネストしたオブジェクトのプロパティは変更できてしまいます。深いレベルまで変更不可にしたい場合は、サードパーティのライブラリや再帰的な型を使う必要があります。

ユーティリティ型の組み合わせ

ユーティリティ型は組み合わせるとさらに強力です。

interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "user";
  createdAt: Date;
}

// 更新APIの入力型:id は必須、他はオプショナル、createdAt は含めない
type UpdateUserInput = Partial<Omit<User, "id" | "createdAt">> & { id: number };

// 使用例
const update: UpdateUserInput = {
  id: 1,              // 必須
  name: "新しい名前",  // オプショナル
  // email, role は省略可能
};

ReactでのジェネリクスDIO

ジェネリクスは React を TypeScript で書くときに頻繁に登場します。ここで少し先取りして、どこでジェネリクスが使われるか見てみましょう。

import { useState, useRef } from "react";

function App() {
  // useState はジェネリック関数
  const [count, setCount] = useState<number>(0);
  const [user, setUser] = useState<User | null>(null);

  // useRef もジェネリック関数
  const inputRef = useRef<HTMLInputElement>(null);

  return <div>...</div>;
}

useState<number>(0) のように型パラメータを指定することで、countnumber 型、setCount(value: number) => void 型になります。これが型安全なReact開発の基盤です。

次のチャプター(React + TypeScript 基礎)で詳しく扱います。

📝 メモ

多くの場合、TypeScriptは初期値から型を推論してくれるので useState(0) だけで十分です。しかし useState<User | null>(null) のように、初期値が null で後から別の型が入るケースでは明示的に指定する必要があります。

ジェネリクスの考え方のコツ

ジェネリクスは最初は難しく感じますが、以下のように考えるとわかりやすくなります。

  1. 「関数のパラメータの型版」と考える — 値を受け取るのが関数パラメータ、型を受け取るのがジェネリクス
  2. 使う側が型を決める — 定義する側は「どんな型が来るかわからないけど、その型で一貫して処理する」
  3. 制約で「最低限のインターフェース」を要求するextends を使えば、最低限必要なプロパティを保証できる
// 「型のパラメータ」という考え方
function wrap<T>(value: T): { value: T } {
  return { value };
}

// 使う側が string と決めた
const wrapped = wrap("hello"); // { value: string }

// 使う側が number と決めた
const wrapped2 = wrap(42);     // { value: number }
✍ やってみよう:ジェネリックな ApiResponse 型を作る

バックエンドAPIのレスポンスを表すジェネリックな型を作りましょう。

ステップ 1: ApiResponse<T> 型を定義する

// APIレスポンスの共通構造
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: string;
}

ステップ 2: 具体的なデータ型を定義する

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

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

ステップ 3: レスポンスを処理する関数を書く

// ジェネリック関数でレスポンスからデータを取り出す
function handleResponse<T>(response: ApiResponse<T>): T {
  if (response.status >= 400) {
    throw new Error(response.message);
  }
  return response.data;
}

ステップ 4: 実際に使ってみる

// ユーザーAPIのレスポンス
const userResponse: ApiResponse<User> = {
  data: { id: 1, name: "山田太郎", email: "yamada@example.com" },
  status: 200,
  message: "成功",
  timestamp: "2025-01-01T00:00:00Z",
};

const user = handleResponse(userResponse);
// user の型は User(自動推論される)
console.log(user.name); // "山田太郎"

// 商品APIのレスポンス
const productResponse: ApiResponse<Product> = {
  data: { id: 1, title: "TypeScript入門", price: 3000 },
  status: 200,
  message: "成功",
  timestamp: "2025-01-01T00:00:00Z",
};

const product = handleResponse(productResponse);
// product の型は Product
console.log(product.price); // 3000

発展課題: ページネーション付きのレスポンス型 PaginatedResponse<T> を作ってみましょう。

// ヒント:ApiResponse を拡張する
interface PaginatedResponse<T> {
  data: T[];            // 配列になる
  status: number;
  message: string;
  timestamp: string;
  pagination: {
    currentPage: number;
    totalPages: number;
    totalItems: number;
    perPage: number;
  };
}

まとめ

次のチャプターでは、いよいよ React + TypeScript の基礎に入ります。コンポーネントの Props、State、イベントに型をつける方法を学んでいきましょう。