【TypeScript】ジェネリクス入門:型を引数のように扱う

JavaScript

こんにちは、かつコーチです。

前回は、nullundefined を安全に扱う strictNullChecks について解説しました。

今回からは少し応用的な内容に入っていきます。

最初のテーマは、TypeScriptを学んでいると必ずぶつかる壁「ジェネリクス」です。

見慣れない <T> という記号にとまどう人も多いと思いますが、考え方はシンプルです。

今日は、ジェネリクスを身近な例からやさしく解説していきます。

ジェネリクスとは何か

「型を引数のように扱う仕組み」

ジェネリクス(Generics) とは、ひとことで言うと「型を引数のように扱う仕組み」です。

関数に値を渡すのと同じように、関数や型に「どんな型を使うか」を渡せるようにする機能だとイメージしてください。

まずはジェネリクスを使わない、素朴なコードから見てみましょう。

function getFirstNumber(list: number[]): number {
  return list[0];
}

function getFirstString(list: string[]): string {
  return list[0];
}

console.log(getFirstNumber([1, 2, 3]));           // 1
console.log(getFirstString(["a", "b", "c"]));     // "a"

「配列の先頭要素を返す」という処理は、数値でも文字列でもまったく同じロジックです。

しかし、扱う型ごとに関数を作り直すのは無駄が多く、boolean の配列やオブジェクトの配列が増えるたびに、同じような関数を量産することになってしまいます。

身近な例で理解するジェネリクス

ここでジェネリクスを使うと、1つの関数でどんな型の配列にも対応できます。

function getFirst<T>(list: T[]): T {
  return list[0];
}

console.log(getFirst<number>([1, 2, 3]));        // 1
console.log(getFirst<string>(["a", "b", "c"]));  // "a"
console.log(getFirst([true, false]));            // true(型推論により<T>は省略可)

<T>T は「Type(型)」の頭文字で、ここに実際の型がはめ込まれるプレースホルダーです。

getFirst<number> と書けば Tnumber に、getFirst<string> と書けば Tstring に置き換わるイメージです。

「箱を用意しておいて、使うときに中身の種類を後から決める」——これがジェネリクスの正体です。

私が最初にジェネリクスを勉強したときは、「なぜ any を使わないんだろう」と疑問に思っていました。

次の章で、any との違いを具体的に見ていきます。

anyとの違い:型の情報を失わない

any型を使った場合の問題点

「どんな型でも受け取りたいなら any でいいのでは?」というのは、私も最初につまずいたポイントです。

❌ Before:any型を使うと型情報が失われる

function getFirstAny(list: any[]): any {
  return list[0];
}

const result = getFirstAny([1, 2, 3]);
console.log(result.toFixed(2)); // 一見動きそうに見えるが、resultの型はany

const result2 = getFirstAny(["a", "b", "c"]);
console.log(result2.toFixed(2)); // ❌ 実行時エラー:文字列にtoFixedは存在しない

any を使うと、戻り値の型も any になってしまい、TypeScriptによる型チェックが実質的に効かなくなります。

result2 のように、本来は文字列なのに数値用のメソッド toFixed を呼び出すコードでも、コンパイル時にはエラーが出ません。

実行して初めてエラーに気づく、という状況になってしまいます。

ジェネリクスを使った場合

✅ After:ジェネリクスで型情報を保ったまま扱う

function getFirst<T>(list: T[]): T {
  return list[0];
}

const result = getFirst([1, 2, 3]);
console.log(result.toFixed(2)); // OK:resultはnumber型と推論される

const result2 = getFirst(["a", "b", "c"]);
console.log(result2.toFixed(2)); // ❌ コンパイルエラー:string型にtoFixedは存在しない

ジェネリクスを使った場合、渡した配列の型(この例では number[]string[])に応じて T が自動的に決まり、戻り値の型もそれに合わせて決まります。

result2 の間違いは、実行前のコンパイル時点でしっかりエラーとして検出されます。

「どんな型でも受け取れる柔軟さ」と「型の安全性」を両立できるのが、ジェネリクスの最大のメリットです。

配列以外でもジェネリクスは使える

複数の型引数を持つ場合

ジェネリクスは1つだけでなく、複数使うこともできます。

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

const pair1 = makePair<string, number>("かつコーチ", 30);
console.log(pair1); // ["かつコーチ", 30]

const pair2 = makePair(true, "初心者向け"); // 型推論により指定を省略できる
console.log(pair2); // [true, "初心者向け"]

TU という2つの型引数を用意することで、「1つ目と2つ目で違う型を受け取りたい」場合にも対応できます。

インターフェースでのジェネリクス

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

interface ApiResponse<T> {
  status: number;
  data: T;
}

const userResponse: ApiResponse<{ id: number; name: string }> = {
  status: 200,
  data: { id: 1, name: "かつコーチ" },
};

const productListResponse: ApiResponse<string[]> = {
  status: 200,
  data: ["ノートPC", "マウス", "キーボード"],
};

APIのレスポンスのように「型は違っても構造は同じ」というデータに対して、ApiResponse<T> のような形でジェネリクスを使うと、data の中身がユーザー情報でも商品一覧でも、同じ interface を使い回せます。

私が実務で最初にジェネリクスの便利さを実感したのは、まさにこのAPIレスポンスの型定義でした。

エンドポイントごとに UserApiResponseProductApiResponse と型を量産していたのを、ApiResponse<T> ひとつにまとめられたときは、かなりスッキリした感覚がありました。

つまずきやすいポイント

Tという名前に深い意味はない

ジェネリクスを学び始めたころ、私は T という文字自体に何か特別な意味があると思い込んでいました。

実際には T はただの変数名(型引数名)で、T である必要はありません。

// TでもUでもDataでも、何でもよい(読みやすい名前を選べばよい)
function getFirst<Item>(list: Item[]): Item {
  return list[0];
}

慣習として、1文字目は T、2つ目は U、要素なら Item、レスポンスなら Response のように意味の伝わる名前を使うことが多いですが、文法上のルールがあるわけではありません。

型引数の指定を省略しすぎて読みにくくなる

もう一つ、実務で気をつけたいのが「型推論に頼りすぎて、読み手が型を追いにくくなる」ケースです。

// 型推論に完全に頼った場合
const result = getFirst([]);

空配列を渡すと、TypeScriptは Tnever 型(何にも当てはまらない型)と推論してしまい、意図と違う挙動になることがあります。

// 型を明示することで意図をはっきりさせる
const result = getFirst<string>([]);

型推論は便利ですが、「配列が空になりうる」「複数の型が混ざりうる」といった曖昧さがある場面では、型引数を明示的に指定した方が安全です。

まとめ

この記事のポイント

  • ジェネリクスは「型を引数のように扱う仕組み」で、<T> は型を後から決めるプレースホルダー
  • any と違い、ジェネリクスは型の安全性を保ったまま柔軟な関数・型を定義できる
  • 関数だけでなく、interfacetype にもジェネリクスを使える
  • T という名前自体に特別な意味はなく、読みやすい名前を選んでよい
  • 型推論が曖昧になりそうな場面では、型引数を明示的に指定すると安全

次に読むべき記事

次回は、ジェネリクスの制約(extends)を使いこなす方法を解説します。

→ 次の記事:ジェネリクスの制約(extends)を使いこなす

タイトルとURLをコピーしました