TypeScriptのコードを読んでいると、<T> という見慣れない記号が出てきて「これ何…?」となることがありますよね。

<T> って何?型名なの?変数なの?
ジェネリクスって聞いたことあるけど、意味がよくわからない
anyで書けばいいんじゃないの?わざわざジェネリクスを使う理由がわからない

この記事では、TypeScriptのジェネリクスについて「なぜ必要なのか」「どう書くのか」「どんな場面で使うのか」の3点を順番に説明します。コードを読んでいて <T> が出てきてつまずいた方でも、読み終わる頃には「あー、こういうことか」と腹落ちできるはずです。

この記事は次のような方におすすめです。

この記事はこんな人におすすめ!
  • TypeScriptの基本的な型(string / number / boolean)やinterfaceは知っているが、ジェネリクスはまだよくわからない方
  • コードの中に <T><U> が出てきて意味がわからなかった方
  • 「anyで書けばいいのでは?」という疑問を持っている方
  • ジェネリクスをどんな場面で使えばいいか判断できない方

この記事を読むと、<T> の意味がわかり、関数やinterfaceにジェネリクスを使えるようになります。「どんな場面で使えばいいか」の判断基準も整理するので、コードを読むときだけでなく書くときにも役立ててもらえるはずです。

それでは、順を追って詳しく見ていきましょう!

ジェネリクスとは何か — 型を「後から決める」しくみ

ジェネリクスとは何か — 型を「後から決める」しくみ

ジェネリクスとは、型をあとから外側から渡せるようにする仕組みのことです。

たとえば「何かを受け取って、そのまま返す関数」を作りたいとします。受け取るものが string のときもあれば、number のときもある。このような関数を型安全に書こうとすると、通常の書き方では困ることが出てきます。その「困ること」から見ていきましょう。

anyで書くと何が起きるか

「どんな型でも受け取れるようにしたい」と思って any を使うと、こんな感じになります。

function identity(arg: any): any {
  return arg;
}

const result = identity("hello");
// result の型は any になってしまう
result.toUpperCase(); // エラーにならないが...
result.toFixed(2);   // これもエラーにならない(本来は文字列なのに)

"hello" を渡したのに、result の型が any になっています。そのせいで、数値専用のメソッド .toFixed() を呼んでもTypeScriptが何も言ってくれません。実行してみて初めてエラーに気づく、という状況になってしまいます。

anyについてさらに詳しく知りたい方は、[TypeScriptのanyとunknownの違い]の記事もあわせて読んでみてください。

ジェネリクスを使うと何が変わるか

同じ関数をジェネリクスで書くとこうなります。

function identity<T>(arg: T): T {
  return arg;
}

let result = identity("hello");
// result の型は string として推論される
result.toUpperCase(); // OK
result.toFixed(2);   // エラー!string に toFixed はない

result の型がちゃんと string になり、おかしな操作をするとコンパイル時点でエラーが出ます。型安全性を保ちながら、どんな型でも使える汎用的な関数を作れるのがジェネリクスの価値です。

基本の書き方 — 関数ジェネリクスから始める

基本の書き方 — 関数ジェネリクスから始める

概念がつかめたところで、実際の書き方を見ていきましょう。

<T> の読み方と書き方

ジェネリクスの基本構文はこうです。

function identity<T>(arg: T): T {
  return arg;
}

この <T> を分解すると、

  • <T> :「これはジェネリクス関数で、型引数の名前は T ですよ」という宣言
  • arg: T :引数の型は T(後から決まる)
  • : T :戻り値の型も T(引数と同じ型を返す)

という意味になります。T は「この関数を呼び出す時点で決まる型」のプレースホルダーだと考えると理解しやすいです。

呼び出すときはこんな感じです。

identity<string>("hello"); // T = string として呼び出す
identity<number>(42);      // T = number として呼び出す

型引数の名前はなぜ T なのか

「T じゃないといけないの?」と思うかもしれませんが、T でなくてもかまいません。 T は Type(型)の頭文字を取った慣習的な名前です。

よく使われる慣習的な名前を整理すると、こんな感じです。

名前 意味の由来 よく使われる場面
T Type 汎用的な型引数全般
K Key オブジェクトのキー
V Value オブジェクトの値
E Element 配列の要素
U T の次に使う2つ目の型引数
なかむぅ
なかむぅ
慣習に従っておくと他の人がコードを読みやすくなるので、特別な理由がなければ T から使うのが無難です。

型推論による型引数の省略

実は、<string> のように型引数を明示しなくても、TypeScriptが渡した値から型を推論してくれます。

const str: string = "hello";

const result1 = identity<string>(str); // 明示する場合。T = string
const result2 = identity(str);         // 省略した場合。T = string と推論される

引数から型が明らかなときは省略するほうがコードがすっきりします。

ただし、推論が効かないケースもあります。たとえば型引数が引数の型と無関係な場合です。

// T が引数の型と対応していないので、推論できない
function createEmpty<T>(): T[] {
  return [];
}

const arr = createEmpty();          // T が不明なので unknown[] になる
const arr2 = createEmpty<string>(); // 明示が必要

このように「返り値にだけ型引数が登場する」パターンでは、渡す引数から T を推論する手がかりがないので、明示的に <string> と書く必要があります。

なかむぅ
なかむぅ
型推論については以下の記事で詳しく解説しています。
【TypeScript】型推論ってなに?型を書かなくても型が決まる仕組みを解説
【TypeScript】型推論ってなに?型を書かなくても型が決まる仕組みを解説TypeScriptの型推論を初心者向けにやさしく解説。型を書かなくても型が決まる仕組みや、省略できる場面・書くべき場面がすっきりわかります。 ...

interfaceにジェネリクスを使う

interfaceにジェネリクスを使う

関数だけでなく、interface にもジェネリクスは使えます。

オブジェクト型を汎用化したい場面

よくある場面として「APIのレスポンス型」があります。APIによってレスポンスの data 部分の型が異なることはよくあることです。

// ユーザー情報を返すAPIのレスポンス
{ status: "ok", data: { id: 1, name: "Taro" } }

// 商品情報を返すAPIのレスポンス
{ status: "ok", data: { id: 101, price: 500 } }

status は共通ですが、data の型が違います。それぞれ別のinterfaceを作ると似たようなコードが増えてしまいます。

interfaceにジェネリクスを使う書き方

interface ApiResponse<T> {
  status: "ok" | "error";
  data: T;
}

// 使うとき
interface User {
  id: number;
  name: string;
}

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

const userResponse: ApiResponse<User> = {
  status: "ok",
  data: { id: 1, name: "Taro" },
};

const productResponse: ApiResponse<Product> = {
  status: "ok",
  data: { id: 101, price: 500 },
};

ApiResponse<User> のように呼び出すことで、data の型が User に確定します。1つのinterfaceを使い回せるので、似た構造の型を量産せずに済みます。

型引数に制約をつける — extends の基本

型引数に制約をつける — extends の基本

ここからは少し応用的な話です。型引数はデフォルトで「どんな型でも受け付ける」状態になっています。それが困る場面もあります。

制約なしだと何が困るか

たとえば「オブジェクトの name プロパティを取り出す関数」を作りたいとします。

function getName<T>(obj: T): string {
  return obj.name; // エラー!T に name があるかわからない
}

T がどんな型でも受け付ける状態なので、「name プロパティを持つかどうか」が保証されません。TypeScriptがエラーを出します。

extends で受け付ける型を絞る

<T extends ...> という書き方で、型引数に制約をつけられます。

interface HasName {
  name: string;
}

function getName<T extends HasName>(obj: T): string {
  return obj.name; // OK!T は name を持つことが保証されている
}

getName({ name: "Taro", age: 25 }); // OK
getName({ age: 25 });               // エラー!name がない

T extends HasName とすることで「name: string を持つ型しか受け付けない」という制約がつきます。これで型の安全性を保ちながら、汎用的な関数が書けます。

複数の型引数を使う

複数の型引数を使う

型引数は複数持たせることもできます。<T, U> のように , で区切ります。

function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const result = pair("hello", 42);
// result の型は [string, number] になる

TU がそれぞれ独立した型引数になっていて、引数から型推論が効いています。複数の型が絡む処理を型安全に書きたいときに使います。

どんな場面でジェネリクスを使えばいいか

どんな場面でジェネリクスを使えばいいか

「書き方はわかったけど、実際にどこで使うか判断できない」という疑問が残るかもしれません。使いどころはだいたい次の3つです。

① 同じ処理を、型を変えて使い回したいとき

型ごとに関数を量産するのが面倒なとき、ジェネリクスで1本にまとめられます。

// number[] の先頭を取る関数と string[] の先頭を取る関数を別々に書く必要がない
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

first([1, 2, 3]);         // number | undefined
first(["a", "b", "c"]);  // string | undefined

② 型の構造は共通で、一部だけ変わるとき

「interfaceにジェネリクスを使う」の節で紹介した ApiResponse<T> がこれです。status などの共通部分は固定しつつ data の型だけ呼び出し側に任せる形にできます。User を返すAPIでも Product を返すAPIでも、同じinterfaceを使い回せます。

③ 共通のプロパティを持つ複数の型をまとめて扱いたいとき(extends の出番)

「User でも Admin でも、name を持っていればこの関数に渡せる」という場合です。

interface HasName {
  name: string;
}

function greet<T extends HasName>(obj: T): string {
  return `Hello, ${obj.name}!`;
}

greet({ name: "Taro", age: 25 });        // OK(User 的なオブジェクト)
greet({ name: "Hanako", role: "admin" }); // OK(Admin 的なオブジェクト)
greet({ age: 25 });                      // エラー!name がない

any にすると name があるかどうかの保証が消えますが、extends を使えば「name を持つ型ならなんでも受け付ける」という柔軟性と型安全性を両立できます。

なかむぅ
なかむぅ
逆に、特定の型しか扱わない処理ならジェネリクスは不要です。string だけ受け取る関数なら arg: string と書きましょう。汎用化が必要な場面に限って使うのが、ジェネリクスのスマートな使い方です。

【付録】さらに学びを深めるためのリソース


さらにTypescriptの学習を進めたい方のために、いくつかのリソースを紹介します。
これらのリソースを活用することで、TypeScriptの型システムについてより深い知識を得ることができるでしょう。

おすすめの書籍

ゼロからわかる TypeScript入門


技術評論社から出版されている「ゼロからわかる TypeScript入門」は、プログラミング初心者や本職プログラマーではない方を主な対象にした入門書です。

変数・条件分岐・ループといった基本から、クラスやインターフェース、モジュールまで段階的に学べる構成になっています。最終章ではWeb APIとJSONを使った非同期Webアプリの作成も体験できるので、「実際に動くものを作る」ところまで到達できます。

プロを目指す人のためのTypeScript入門


技術評論社の「プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで」、通称 ブルーベリー本 です。
JavaScriptの仕様とTypeScript独自の機能を両方押さえつつ、リテラル型・ユニオン型・keyof型・ジェネリクスなど、高度な型表現まで踏み込んで解説しています。TypeScriptの型システムの表現力を本格的に学べる一冊です。

オンラインで参照できる公式ドキュメント

TypeScript公式ハンドブック


https://www.typescriptlang.org/docs/
TypeScriptの公式ドキュメントです。
intersection型を含む、すべての型システムの機能について詳細な説明があります。

TypeScript Deep Dive


https://basarat.gitbook.io/typescript/
TypeScriptの深い部分まで掘り下げて解説しているオンラインブックです。
無料で読むことができ、intersection型についても詳しく説明されています。

TypeScriptの学習は終わりがありません。
新しい機能が常に追加され、より良い書き方が発見されています。
継続的に学習を続けることで、より良いTypeScriptプログラマーになれるはずです。


まとめ:ジェネリクス(generics)で押さえておくこと

まとめ:ジェネリクス(generics)で押さえておくこと

この記事で説明したことをまとめます。

  • ジェネリクスとは「型を後から外側から渡せるようにする仕組み」
  • anyとの違いは「型安全性が保たれるかどうか」
  • <T> は型引数のプレースホルダー。T はあくまで慣習的な名前
  • 型推論が効く場面では型引数を省略できる
  • interface にも使えて、共通の型定義を再利用できる
  • extends で型引数に制約をつけることで、より安全に書ける

ジェネリクスは最初とっつきにくく見えますが、「汎用的な型テンプレート」という感覚で捉えると使いどころが見えてきます。最初は「anyを使いたい場面でジェネリクスが使えないか」と考えてみるだけでも、少しずつ実感が掴めてくるはずです。

※本記事の本文案はAIを活用して作成していますが、記載している内容およびコードは筆者が実際に調査、検証・実行し、内容の正確性を確認した上で公開しています。