こんにちは、かつコーチです。
前回は、null と undefined を安全に扱う 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> と書けば T が number に、getFirst<string> と書けば T が string に置き換わるイメージです。
「箱を用意しておいて、使うときに中身の種類を後から決める」——これがジェネリクスの正体です。
私が最初にジェネリクスを勉強したときは、「なぜ 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, "初心者向け"]
T と U という2つの型引数を用意することで、「1つ目と2つ目で違う型を受け取りたい」場合にも対応できます。
インターフェースでのジェネリクス
ジェネリクスは関数だけでなく、interface や type にも使えます。
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レスポンスの型定義でした。
エンドポイントごとに UserApiResponse、ProductApiResponse と型を量産していたのを、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は T を never 型(何にも当てはまらない型)と推論してしまい、意図と違う挙動になることがあります。
// 型を明示することで意図をはっきりさせる
const result = getFirst<string>([]);
型推論は便利ですが、「配列が空になりうる」「複数の型が混ざりうる」といった曖昧さがある場面では、型引数を明示的に指定した方が安全です。
まとめ
この記事のポイント
- ジェネリクスは「型を引数のように扱う仕組み」で、
<T>は型を後から決めるプレースホルダー anyと違い、ジェネリクスは型の安全性を保ったまま柔軟な関数・型を定義できる- 関数だけでなく、
interfaceやtypeにもジェネリクスを使える Tという名前自体に特別な意味はなく、読みやすい名前を選んでよい- 型推論が曖昧になりそうな場面では、型引数を明示的に指定すると安全
次に読むべき記事
次回は、ジェネリクスの制約(extends)を使いこなす方法を解説します。
→ 次の記事:ジェネリクスの制約(extends)を使いこなす
