ゴール:「型を“あとから差し込める穴”として扱う感覚をつかむ」
ジェネリクス(Generics)は一言でいうと、
「型に“変数”を導入して、あとから具体的な型を差し込める仕組み」
です。
関数でいうと、(x: number) => number の number を
「あとで決められるようにする」のがジェネリクス、というイメージです。
「型の再利用性」と「型安全」を同時に上げるための道具だと思ってください。
まずは“ジェネリックじゃない”関数から見る
number 専用の関数
例えば、配列の先頭要素を返す関数を考えます。
function firstNumber(arr: number[]): number | undefined {
return arr[0];
}
const n = firstNumber([1, 2, 3]); // n は number | undefined
TypeScriptこれは「number の配列専用」です。
string でも使いたくなったら、こう増えていきます。
function firstString(arr: string[]): string | undefined {
return arr[0];
}
TypeScript型ごとに関数を増やすのは、明らかにダルいですよね。
any を使うとどうなるか
「じゃあ any にしちゃえば?」とやるとこうなります。
function firstAny(arr: any[]): any {
return arr[0];
}
const x = firstAny([1, 2, 3]); // x: any
const y = firstAny(["a", "b", "c"]); // y: any
TypeScriptどんな配列でも受け取れるようにはなりましたが、
戻り値の型が any になってしまい、型安全性が消えます。
y.toUpperCase() と書いても、
コンパイラは何も教えてくれません。
ジェネリクス登場:「型の穴」を用意する
T という“型の変数”を導入する
ここでジェネリクスの出番です。
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // n: number | undefined
const s = first(["a", "b", "c"]); // s: string | undefined
TypeScript<T> が「型の変数」です。
T は「なんでもいいけど、呼び出し時に決まる型」です。
first<number>([1, 2, 3]) と書いてもいいし、
TypeScript に推論させて first([1, 2, 3]) と書いても OK です。
ここで重要なのは、
「関数の中身は T という“抽象的な型”で書いておき、
呼び出し時に T に具体的な型(number や string)が入る」
という構造です。
any との決定的な違い
any とジェネリクスの違いをはっきりさせましょう。
function firstAny(arr: any[]): any {
return arr[0];
}
function firstGeneric<T>(arr: T[]): T | undefined {
return arr[0];
}
TypeScriptfirstAny は「何を渡しても戻り値は any」。
型情報が消えます。
firstGeneric は「渡した配列の要素型を、そのまま戻り値に反映」します。
firstGeneric([1, 2, 3]) の戻り値は number | undefinedfirstGeneric(["a", "b"]) の戻り値は string | undefined
というふうに、
「呼び出しごとに型がちゃんと変わる」のがジェネリクスの強みです。
ジェネリクスの基本構文と読み方
関数のジェネリクス
一番よく見る形はこれです。
function identity<T>(value: T): T {
return value;
}
const a = identity<number>(1); // a: number
const b = identity("hello"); // b: string(推論される)
TypeScript読み方としては、
「identity は、型パラメータ T を受け取る関数。
引数 value は T 型で、戻り値も T 型。」
です。
つまり、
「T という“型の穴”を用意しておいて、呼び出し時にそこに具体的な型を入れる」
というイメージで読めれば OK です。
型パラメータは複数あってもいい
例えば、2つの配列を受け取ってタプルにする関数。
function pair<T, U>(a: T, b: U): [T, U] {
return [a, b];
}
const p1 = pair(1, "a"); // [number, string]
const p2 = pair(true, { x: 1 }); // [boolean, { x: number }]
TypeScriptここでは、
- T は第1引数の型
- U は第2引数の型
- 戻り値は
[T, U]というタプル型
になっています。
「型の変数が複数あるだけで、やっていることは同じ」です。
クラスでのジェネリクス
ジェネリックなコンテナクラス
例えば、「どんな型でも入れられる箱」をクラスで表現してみます。
class Box<T> {
constructor(private value: T) {}
getValue(): T {
return this.value;
}
}
const numberBox = new Box<number>(123);
const n = numberBox.getValue(); // n: number
const stringBox = new Box("hello"); // T は string と推論される
const s = stringBox.getValue(); // s: string
TypeScriptここでも <T> が「型の穴」です。
Box<number> は「number を入れる箱」Box<string> は「string を入れる箱」
というふうに、
同じクラス定義から、型の違うバリエーションをいくつも作れます。
重要なのは、
「クラスの中では T としか書いていないのに、
実際には number や string として振る舞う」
というところです。
ジェネリクスが“嬉しい”と感じる瞬間
「型ごとに同じ関数を量産しなくてよくなる」
さっきの firstNumber / firstString のように、
型ごとに関数を増やす必要がなくなります。
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
TypeScriptこの 1 本で、
first<number>(number[])first<string>(string[])first<User>(User[])
など、いくらでも使い回せます。
「ロジックは同じで、型だけ違う」
という場面で、ジェネリクスは本領を発揮します。
「型安全を保ったまま汎用化できる」
any を使えば汎用化はできますが、
型安全性を失います。
ジェネリクスは、
「汎用性」と「型安全」を両立させるための仕組み
です。
例えば、配列を逆順にする関数。
function reverse<T>(arr: T[]): T[] {
return [...arr].reverse();
}
const nums = reverse([1, 2, 3]); // number[]
const strs = reverse(["a", "b", "c"]); // string[]
TypeScript戻り値の型がちゃんと「元の配列の要素型」に合わせて変わるので、
その後のコードでも型の恩恵を受け続けられます。
もう一歩:ジェネリクスに“制約”をつける
T に「なんでも入れていい」わけではない場合
例えば、「length プロパティを持つものだけ受け取りたい」関数。
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
getLength("hello"); // OK(string は length を持つ)
getLength([1, 2, 3]); // OK(配列も length を持つ)
getLength({ length: 10 }); // OK
// getLength(123); // エラー:number は length を持たない
TypeScriptT extends { length: number } が「型の制約」です。
ここでやっていることは、
「T はなんでもいいけど、少なくとも length: number を持っていてね」
という条件をつけている、ということです。
これにより、
- 関数の中では
value.lengthを安心して使える - 呼び出し側は「length を持たない型」を渡すとコンパイルエラーになる
という、気持ちいい状態になります。
まとめ:ジェネリクスとは何かを自分の言葉で説明すると
最後に、あなた自身の言葉でこう整理してみてください。
ジェネリクスとは、
「型に“変数”を導入して、あとから具体的な型を差し込める仕組み」。<T> のような型パラメータを使うことで、
1つの関数・クラス・型定義を、いろいろな型に対して再利用できる。
any と違って、
「呼び出しごとに具体的な型が保たれる」ので、
汎用性と型安全を両立できる。
まずは、
function identity<T>(value: T): T { return value; }
class Box<T> { constructor(private v: T) {} getValue(): T { return this.v; } }
TypeScriptこの 2 つを、自分の手で書いて、T に number や string を差し込んで遊んでみてください。
そこで、
「T は“あとで決まる型の穴”なんだな」
と体で感じられたら、ジェネリクスの入口はもう突破できています。
