ジェネリクス
型をパラメータ化して再利用可能なコードを書く方法を学びます
このチャプターで学ぶこと
- ジェネリクスの基本概念を理解できる
- ジェネリック関数を作成できる
- 制約(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のコレクション 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> は「T は HasLength インターフェースを満たす型でなければならない」という意味です。
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 }); // 複数フィールドを更新
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) のように型パラメータを指定することで、count が number 型、setCount が (value: number) => void 型になります。これが型安全なReact開発の基盤です。
次のチャプター(React + TypeScript 基礎)で詳しく扱います。
多くの場合、TypeScriptは初期値から型を推論してくれるので useState(0) だけで十分です。しかし useState<User | null>(null) のように、初期値が null で後から別の型が入るケースでは明示的に指定する必要があります。
ジェネリクスの考え方のコツ
ジェネリクスは最初は難しく感じますが、以下のように考えるとわかりやすくなります。
- 「関数のパラメータの型版」と考える — 値を受け取るのが関数パラメータ、型を受け取るのがジェネリクス
- 使う側が型を決める — 定義する側は「どんな型が来るかわからないけど、その型で一貫して処理する」
- 制約で「最低限のインターフェース」を要求する —
extendsを使えば、最低限必要なプロパティを保証できる
// 「型のパラメータ」という考え方
function wrap<T>(value: T): { value: T } {
return { value };
}
// 使う側が string と決めた
const wrapped = wrap("hello"); // { value: string }
// 使う側が number と決めた
const wrapped2 = wrap(42); // { value: number }
バックエンド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;
};
} まとめ
- ジェネリクスは「型のパラメータ」。関数の引数と同じ発想で、型を外から渡せる
<T>で型パラメータを宣言し、関数やインターフェースを汎用的にできる<T extends SomeType>で制約をつけて、型パラメータに最低限の条件を要求できる- 複数の型パラメータ
<T, U>も指定可能 - 組み込みユーティリティ型
Partial,Required,Pick,Omit,Record,Readonlyを活用すると既存の型から新しい型を作れる - React の
useState<T>やuseRef<T>もジェネリクスの活用例
次のチャプターでは、いよいよ React + TypeScript の基礎に入ります。コンポーネントの Props、State、イベントに型をつける方法を学んでいきましょう。