TypeScript で「どんな型でも受け取れて、しかも型の安全性は失いたくない」という場面に出会ったことはないでしょうか。そのための仕組みがジェネリクス(Generics)です。ジェネリクスを使うと、型を「あとから決められる部品」として扱えるようになり、同じ関数やクラスをさまざまな型で安全に再利用できます。この記事では、なぜ any ではダメなのかという理由から、基本構文、配列やインターフェースでの使い方、複数の型引数、extends による制約、そして実際のユースケースまでを、初心者〜中級者向けに順を追って解説します。
目次
なぜジェネリクスが必要なのか
たとえば「受け取った値をそのまま返す関数」を作るとします。文字列にも数値にも使いたいので、つい any 型を使いたくなります。しかし any を使うと、その値が本来どんな型だったかという情報が失われてしまいます。
// any を使うと型情報が消える
function identity(arg: any): any {
return arg;
}
const result = identity('hello');
// result は any 型なので、下のような間違いも
// コンパイル時に気づけない
result.toFixed(2); // 文字列なのに数値メソッドを呼んでいるが、エラーにならない
この例では result は本当は文字列なのに、型が any になっているため toFixed(数値用のメソッド)を呼んでもエラーになりません。実行時に初めて壊れる、TypeScript を使う意味が薄れてしまうコードです。ジェネリクスは、こうした場面で「渡した型」と「返る型」を結びつけたまま、複数の型に対応できるようにする仕組みです。
ジェネリクスの基本構文
ジェネリクスは、関数名のうしろに <T> のように型引数(型パラメータ)を書きます。T は「あとで決まる型の名前」を表すプレースホルダーで、関数を呼び出したときに実際の型に置き換わります。先ほどの identity をジェネリクスで書き直してみましょう。
// <T> が型引数。arg の型と戻り値の型を T で結びつける
function identity<T>(arg: T): T {
return arg;
}
// 呼び出し方1: 型推論にまかせる(推奨)
const a = identity('hello'); // T は string と推論され、a は string
const b = identity(123); // T は number と推論され、b は number
// 呼び出し方2: 型を明示する
const c = identity<string>('hello'); // T を string と明示
identity<T>(arg: T): T は「引数 arg の型と戻り値の型が同じ T である」という宣言です。identity('hello') と呼ぶと T は自動的に string になり、戻り値も string 型になります。any と違って型情報が保たれるので、a.toFixed(2) のような間違いはコンパイル時にエラーとして検出されます。ほとんどの場合、型引数は引数から自動で推論されるため、identity<string>(...) のように明示する必要はありません。推論がうまくいかないときだけ明示すると覚えておきましょう。
なお型引数の名前は自由ですが、慣習として単一の型には T(Type の頭文字)がよく使われます。用途に応じて次のような名前もよく見かけます。
| 名前 | 由来・よく使われる場面 |
|---|---|
T | Type。もっとも一般的な型引数 |
U, V | 2つ目・3つ目の型引数(T の次のアルファベット) |
K | Key。オブジェクトのキーの型に使う |
V | Value。値の型に使う |
E | Element。配列などの要素の型に使う |
配列やインターフェースで使う
ジェネリクスは、実は普段から使っている型の中にも登場します。配列の型 Array<T> がその代表例です。string[] という書き方は Array<string> の省略形で、「string を要素に持つ配列」を表しています。
// この2つは同じ意味
const names1: string[] = ['Taro', 'Hanako'];
const names2: Array<string> = ['Taro', 'Hanako'];
// 配列の要素を1つ返す汎用関数
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = firstElement([1, 2, 3]); // n は number | undefined
const s = firstElement(['a', 'b', 'c']); // s は string | undefined
自分でジェネリックな型を定義することもできます。よく使うのが、値を包む「入れ物」のインターフェースです。次の Box<T> は、中身の型を T として受け取ります。
interface Box<T> {
value: T;
}
// 使うときに T を具体的な型で指定する
const stringBox: Box<string> = { value: 'hello' };
const numberBox: Box<number> = { value: 42 };
// stringBox.value は string 型なので安全に扱える
console.log(stringBox.value.toUpperCase());
Box<string> と書けば value は string に、Box<number> と書けば value は number になります。1つの定義から、中身の型が違う複数の型を作り出せるのがジェネリクスの強みです。
複数の型引数を使う
型引数は1つだけでなく、<T, U> のようにカンマで区切って複数持たせることもできます。たとえば2つの値をペアにして返す関数では、それぞれの型を別々に扱いたいので型引数を2つ使います。
// 2つの値をタプルにして返す
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const p = pair('age', 20);
// p は [string, number] 型
const label = p[0]; // string
const value = p[1]; // number
pair('age', 20) では T が string、U が number と推論され、戻り値は [string, number] というタプル型になります。それぞれの引数の型を独立して保てるので、異なる型の組み合わせでも正確に扱えます。
extends で型引数に制約をかける
ジェネリクスは「どんな型でも受け取れる」のが便利ですが、逆に何でも受け取れるがゆえに、関数の中で T のプロパティにアクセスできないという制限があります。たとえば「引数の length を返す関数」を素直に書くとエラーになります。
function getLength<T>(arg: T): number {
// エラー: T に length があるとは限らない
return arg.length;
}
T は数値や真偽値かもしれず、length を持つとは限らないため、TypeScript はこのアクセスを拒否します。ここで使うのが extends による型引数の制約です。「T は少なくとも length: number を持つ型に限る」と宣言すれば、安全に length へアクセスできます。
// T は「length: number を持つ型」に制約する
function getLength<T extends { length: number }>(arg: T): number {
return arg.length; // OK: T は必ず length を持つ
}
getLength('hello'); // OK: 文字列は length を持つ
getLength([1, 2, 3]); // OK: 配列も length を持つ
getLength({ length: 5 }); // OK: length プロパティを持つオブジェクト
// getLength(123); // エラー: number は length を持たない
T extends { length: number } は「T は length: number を含む型でなければならない」という条件です。制約を付けても T がどの具体的な型かという情報は保たれるので、文字列を渡せば戻り値の計算元は文字列、配列を渡せば配列、というように型安全は維持されます。ジェネリクスの柔軟さと、必要なプロパティの保証を両立させたいときに extends を使うと覚えておきましょう。
実践的なユースケース:API レスポンスのラッパー型
ジェネリクスが力を発揮する典型例が、API レスポンスの型定義です。多くの API は「共通のメタ情報 + 中身のデータ」という構造を返しますが、中身のデータの型はエンドポイントごとに異なります。この「中身」の部分を型引数にすると、1つのラッパー型をあらゆるレスポンスで使い回せます。
// 共通のレスポンス構造。data の型だけを T で差し替える
interface ApiResponse<T> {
success: boolean;
data: T;
message: string;
}
// 個別のデータ型
interface User {
id: number;
name: string;
}
// User を返すエンドポイント
type UserResponse = ApiResponse<User>;
// User の配列を返すエンドポイント
type UserListResponse = ApiResponse<User[]>;
// fetch をラップした汎用関数
async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
const res = await fetch(url);
return res.json();
}
// 呼び出し側で中身の型を指定すると、data に型が付く
const result = await fetchApi<User>('/api/user/1');
console.log(result.data.name); // data は User 型なので name に安全にアクセスできる
ApiResponse<T> という1つの型を定義しておけば、ApiResponse<User> や ApiResponse<User[]> のように中身だけを差し替えて再利用できます。fetchApi<User>(...) と呼べば result.data が User 型になるため、プロパティ名の打ち間違いなどをコンパイル時に防げます。エンドポイントごとに似たような型を何度も書かずに済むのが、ジェネリクスの実用的なメリットです。
型引数を書きすぎて分かりにくくなったとき
ジェネリクスに慣れてくると、つい何でも型引数にしたくなりますが、使いすぎるとかえって読みにくくなります。判断の目安は、その型引数が2箇所以上で関係し合っているかです。
たとえば「引数の型」と「戻り値の型」をつなぐ、あるいは「複数の引数どうし」を関連づける——このように型どうしの関係を表現したいときにジェネリクスは役立ちます。逆に、型引数が関数の中で1回しか使われず、実質 any と変わらない使い方になっているなら、それはジェネリクスにする必要がないサインです。素直に具体的な型を書いたほうが読みやすくなります。「複数の場所で型を一致させたい」という目的があるかどうかを基準に、使いどころを見極めましょう。
まとめ
ジェネリクスは、型を「あとから決められる部品」として扱い、同じ関数や型を複数の型で安全に再利用するための仕組みです。function identity<T>(arg: T): T のように型引数 <T> を宣言すると、any と違って渡した型の情報を保ったまま、さまざまな型に対応できます。多くの場合は型推論にまかせられ、必要なときだけ identity<string>(...) のように明示します。<T, U> で複数の型を扱い、<T extends { length: number }> で必要なプロパティを保証しつつ制約をかけられます。API レスポンスのラッパー型のように、共通の構造をさまざまなデータ型で使い回したい場面で特に効果的です。まずは identity のような小さな例から手を動かして、型が推論されていく様子を確かめてみてください。