ゴール:「ジェネリクスは“カッコいい飾り”じゃなくて、“必要なときだけ出す道具”だと理解する
ジェネリクスに慣れてくると、ほぼ確実に一度は通る道があります。
それが 「何でもかんでも <T> を付けたくなる病」 です。
でも、ジェネリクスは“書けば書くほど良い”ものではありません。
むしろ、「必要なところにだけ、最小限」 が正解です。
ここでは、過剰ジェネリクスの典型パターンと、
「ここはジェネリクスいらないよね」を見抜く感覚を、例と一緒に整理していきます。
そもそも「過剰ジェネリクス」とは何か
定義を一言でいうと
一言でいうと、
「ジェネリクスを使っているけれど、
実は普通の型(string, number, 具体的な型)で十分な場面」
です。
もう少し分解すると、こんな状態が怪しいです。
型パラメータを増やしているのに、実質 1 種類の型しか使っていない
T にしているけど、呼び出し側が T を変える必要がない
ジェネリクスにしたせいで、コードが読みにくくなっている
つまり、「柔軟性を増やしたつもりが、読みやすさと安全性を落としている」状態です。
典型パターン1:実質固定なのに T にしてしまう
悪い例:何でも T にしてしまう関数
例えば、こういうコード。
function toUpperCaseBad<T>(value: T): T {
// @ts-ignore
return value.toUpperCase();
}
TypeScript一見ジェネリックでカッコよく見えますが、これは完全にアウトです。
この関数は「文字列を大文字にしたい」だけなのに、
T にしてしまったせいで、呼び出し側からはこう見えます。
「T なら何でも渡せるし、T がそのまま返ってくる関数」
でも実際には、string 以外を渡すと壊れます。
本当にやりたいことはこれです。
function toUpperCaseGood(value: string): string {
return value.toUpperCase();
}
TypeScriptここでの重要ポイントは、
「呼び出し側が型を変えられる必要がないなら、ジェネリクスにしない」
ということです。
ジェネリクスは「呼び出し側が型を差し替えられる」ための仕組みです。
差し替える必要がないなら、具体的な型で十分です。
もう少し現実的な例
例えば、ユーザー ID は必ず number だと決めているプロジェクトで、
こんな関数を書いたとします。
function getUserByIdBad<T>(id: T) {
// 実際には number 前提で DB を叩いている…
}
TypeScriptこれも過剰ジェネリクスです。
この関数は「number の ID でユーザーを取ってくる」だけなので、
素直にこう書くべきです。
function getUserByIdGood(id: number) {
// number 前提で OK
}
TypeScript「将来 string になるかもしれないから T にしておこう」は、
だいたいの場合「将来の自分を混乱させるだけ」です。
典型パターン2:型パラメータが多すぎる
悪い例:意味のない T, U, V の乱立
例えば、こんなコード。
function mergeBad<T, U, V>(a: T, b: U, c: V): T & U & V {
return { ...a, ...b, ...c };
}
TypeScript一見「汎用的で強そう」に見えますが、
実際にはこうで十分です。
function mergeBetter<T, U>(a: T, b: U): T & U {
return { ...a, ...b };
}
TypeScript3 つ以上マージしたいなら、呼び出し側でネストすればいいだけです。
const merged = mergeBetter(mergeBetter(a, b), c);
TypeScript型パラメータが増えるほど、
読む側は「T と U と V の関係」を追いかけるコストが増えます。
重要なのは、
「本当に必要な可変部分だけを型パラメータにする」
ということです。
「とりあえず T, U, V を増やしておく」は、
過剰ジェネリクスの典型パターンです。
典型パターン3:ジェネリクスでやっていることが any と変わらない
悪い例:T を使っているようで、実は守れていない
function logAndReturnBad<T>(value: T): T {
console.log(value);
// @ts-ignore
value.extra = 123;
return value;
}
TypeScriptT を使っているように見えますが、
実際には value.extra を勝手に追加していて、
T の構造を完全に無視しています。
これは、型安全性の観点では any とほぼ変わりません。
本来ジェネリクスは、
「T が何であっても、その T のルールを守る」
ための仕組みです。
T を無視してプロパティを足したり、
T に存在しないメソッドを呼んだりしている時点で、
ジェネリクスを使う意味がなくなっています。
良い形に直すなら
例えば、「ログを出して、そのまま返す」だけならこうで十分です。
function logAndReturnGood<T>(value: T): T {
console.log(value);
return value;
}
TypeScript「extra を足したい」なら、
T に制約を付けるか、intersection を使うべきです。
function addExtra<T extends object>(value: T): T & { extra: number } {
return { ...value, extra: 123 };
}
TypeScriptここでは、
T extends object で「オブジェクトであること」を保証し、
戻り値の型も T & { extra: number } と明示しています。
ジェネリクスを使うなら、
「T のルールを守る」「T に対して何をするかを型で表現する」
この 2 つは絶対に外さない方がいいです。
典型パターン4:推論で十分なのに、毎回 <T> を書いてしまう
毎回 <number> と書きたくなる病
例えば、こんな関数があるとします。
function identity<T>(value: T): T {
return value;
}
TypeScriptこれをこう呼ぶ人がいます。
const a = identity<number>(1);
const b = identity<string>("hello");
TypeScriptもちろん間違いではありません。
でも、TypeScript の型推論があるので、
普通はこう書けば十分です。
const a = identity(1); // T = number
const b = identity("hello"); // T = string
TypeScript「毎回 <T> を書く」のは、
ジェネリクスに慣れてきた人ほどやりがちな“過剰さ”です。
重要なのは、
「型推論で十分に安全なら、あえて明示しない」
という姿勢です。
明示するのは、
推論では意図した型にならないときだけでいいです。
明示した方がいいケースとの違い
例えば、これは明示した方がいいケースです。
function parseJson<T>(text: string): T {
return JSON.parse(text) as T;
}
type User = { id: number; name: string };
const user = parseJson<User>('{"id":1,"name":"Taro"}');
TypeScriptここでは、引数から T を推論できないので、<User> を書く意味があります。
逆に、引数から T が素直に決まる関数に対して、
毎回 <T> を書くのは「ノイズを増やしているだけ」になりがちです。
過剰ジェネリクスを避けるためのチェック質問
質問1:「この T は、呼び出し側が“変えたい”場面が本当にあるか?」
もし答えが「ない」なら、
その T は具体的な型にしてしまっていい可能性が高いです。
例えば、
常に string を扱う関数
常に User を扱うリポジトリ
常に number の ID を扱う関数
などは、無理にジェネリクスにしなくて構いません。
質問2:「T を消しても、型安全性は保たれるか?」
例えば、こういう関数。
function sum<T extends number>(a: T, b: T): number {
return a + b;
}
TypeScriptこれ、T いりますか?
正直、こうで十分です。
function sum(a: number, b: number): number {
return a + b;
}
TypeScriptT を消しても型安全性が変わらないなら、
そのジェネリクスは過剰である可能性が高いです。
質問3:「このジェネリクスは、プロジェクト内で“パターン”として再利用されているか?」
例えば、
ApiResponse<T>
ValidationResult<T>
Repository<T>
のように、あちこちで使われているなら、
それは良いジェネリクスです。
逆に、
そのファイルの中の 1 箇所でしか使っていない
しかも T を変えて使っていない
なら、ジェネリクスをやめて具体型にした方が読みやすくなります。
まとめ:「過剰ジェネリクスの回避」を自分の言葉で説明すると
最後に、あなた自身の言葉でこう整理してみてください。
ジェネリクスは、
「呼び出し側が型を差し替えられるようにするための“型の穴”」
であって、
「何となくカッコよくするための飾り」ではない。
だから、
実質 1 種類の型しか使わないなら、具体型で書く
T を消しても型安全性が変わらないなら、ジェネリクスをやめる
推論で十分なところに <T> を書きすぎない
T のルールを無視して any 的に扱わない
このあたりを意識すると、「ちょうどいいジェネリクス」になっていきます。
もし今、自分のコードにジェネリクスがいくつかあるなら、
一つ選んで、こう自問してみてください。
「この <T>、本当に“呼び出し側が変えたい型”になっているか?
T を消しても困らないなら、それは過剰ジェネリクスかもしれない。」
その問いを一度でも通したジェネリクスは、
もう“なんとなく書いた T”ではなく、
ちゃんと意味を持った設計になっていきます。
