ゴール:「ここはジェネリクスにする/しない」を自分で決められるようになること
ジェネリクスを“書ける”ようになった次のステップは、
「どこで使うべきか」「どこでは使うべきでないか」を判断できることです。
これはもう、文法の話ではなく“設計のセンス”の話です。
でも、センスは分解するとルールになります。
ここでは、そのルールをできるだけ言語化していきます。
あなたがこれからコードを書くときに、
「ここはジェネリクスにする価値があるか?」を判断するための軸を、
具体例と一緒に整理していきます。
判断基準1:「呼び出し側が“型を差し替えたい”ニーズがあるか」
差し替えニーズがあるならジェネリクス候補
ジェネリクスの本質は一言でいうと「型の差し替え」です。
だから、まず最初の判断基準はこれです。
「この関数(クラス/interface)は、呼び出し側が“中身の型”を変えて使いたくなるか?」
例えば、配列を変換する関数。
function mapArray<Input, Output>(
arr: Input[],
fn: (value: Input) => Output
): Output[] {
return arr.map(fn);
}
TypeScriptこれは、呼び出し側が
数値配列を文字列配列に変えたい
ユーザー配列を ID 配列に変えたい
など、いろいろな型で使いたくなる関数です。
だからジェネリクスにする価値があります。
逆に、こういう関数はどうでしょう。
function toUpper(value: string): string {
return value.toUpperCase();
}
TypeScriptこれは「文字列を大文字にする」だけで、
呼び出し側が型を変えたい場面はありません。
ここを無理にジェネリクスにするのは過剰です。
function toUpperBad<T>(value: T): T {
// @ts-ignore
return value.toUpperCase();
}
TypeScript判断のポイントは、「この型は、呼び出し側にとって“可変”である必要があるか?」です。
可変である必要があるならジェネリクス候補、
そうでないなら具体型で書いた方が素直です。
セキュリティ視点での差し替えニーズ
例えば、検証結果を表す型。
type ValidationResult<Value> =
| { ok: true; value: Value }
| { ok: false; error: string };
TypeScriptこれは、メールアドレス、パスワード、URL など、
「いろいろな“安全な値”を包みたい」というニーズがあります。
type SafeEmail = { value: string };
type SafePassword = { value: string };
function validateEmail(input: string): ValidationResult<SafeEmail> { ... }
function validatePassword(input: string): ValidationResult<SafePassword> { ... }
TypeScriptここでは、Value を差し替えられることに大きな意味があります。
「検証済みの安全な値」というパターンを、
いろいろなドメインに再利用できるからです。
こういう「安全なパターンをいろいろな型に適用したい」ときは、
ジェネリクスにする価値が高いです。
判断基準2:「共通パターンが“型として”見えているか」
ベタ書きが増えてきたら、パターンを探す
ジェネリクスは、最初から狙って書く必要はありません。
むしろ、最初はベタ書きでいいです。
例えば、こんなコードが増えてきたとします。
type GetUserResponse = {
success: boolean;
data: { id: number; name: string } | null;
errorMessage?: string;
};
type GetProductResponse = {
success: boolean;
data: { id: number; title: string; price: number } | null;
errorMessage?: string;
};
TypeScriptこの時点ではジェネリクスは使っていません。
でも、ここで一歩引いて眺めると、
「success, data, errorMessage の“形”は同じで、
中身の data の型だけ違うな」
というパターンが見えてきます。
そこで、共通部分をジェネリクスとして抜き出します。
type ApiResponse<Data> = {
success: boolean;
data: Data | null;
errorMessage?: string;
};
type GetUserResponse = ApiResponse<{ id: number; name: string }>;
type GetProductResponse = ApiResponse<{ id: number; title: string; price: number }>;
TypeScriptここでの判断基準は、
「同じ“形”をした型が、型だけ違う状態で複数出てきているか?」
です。
形が同じ、中身だけ違う。
このとき初めて、「ジェネリクスにする意味」が生まれます。
形が違うのに無理にジェネリクスにしない
逆に、こういうのはジェネリクスにしない方がいい例です。
type User = { id: number; name: string };
type Product = { id: number; title: string; price: number };
TypeScriptどちらも id を持っているからといって、
無理に一つのジェネリクスにまとめる必要はありません。
// 無理な例
type Entity<T> = { id: number } & T;
TypeScriptこういう抽象化は、
「パターンが本当に“型として”共通しているか?」をよく見てからにした方がいいです。
共通パターンが見えていないのにジェネリクスにすると、
「何を表している型なのか」がかえって分かりにくくなります。
判断基準3:「ジェネリクスにすることで“型安全性”が上がるか」
any を減らせるなら、ジェネリクスの価値が高い
ジェネリクスの大きな役割の一つは、
「any を使わずに柔軟さを保つこと」です。
例えば、プロパティを安全に取得する関数。
悪い例はこうです。
function getPropBad(obj: any, key: string): any {
return obj[key];
}
TypeScriptこれは柔軟ですが、型安全性はゼロです。
ジェネリクスを使うと、こう書けます。
function getProp<T, K extends keyof T>(
obj: T,
key: K
): T[K] {
return obj[key];
}
TypeScriptここでは、
T がオブジェクト全体の型
K extends keyof T で「key は T のプロパティ名のどれか」
戻り値の型は T[K](そのプロパティの型)
という関係が、型で表現されています。
このように、
「ジェネリクスにすることで any を排除できる」
「ジェネリクスにすることで、型の関係性を正確に表現できる」
なら、ジェネリクスにする価値は高いです。
型安全性が変わらないなら、ジェネリクスは不要
逆に、こういうコード。
function sumBad<T extends number>(a: T, b: T): number {
return a + b;
}
TypeScriptこれは、ジェネリクスを使っても使わなくても、
型安全性は変わりません。
function sumGood(a: number, b: number): number {
return a + b;
}
TypeScriptT を消しても安全性が変わらないなら、
ジェネリクスは“飾り”になってしまっています。
判断基準として、
「ジェネリクスにしたことで、何か一つでも“型で守れること”が増えたか?」
を自問してみてください。
増えていないなら、そのジェネリクスは要らない可能性が高いです。
判断基準4:「制約(extends)で“前提条件”を型に刻めるか」
コードが暗黙に持っている前提を、型に出せるなら価値がある
例えば、こういう関数。
function getLengthBad<T>(value: T): number {
// @ts-ignore
return value.length;
}
TypeScriptコードは「length を持つもの」を前提にしていますが、
型にはその前提が一切現れていません。
これをジェネリクス+制約で書き直すと、こうなります。
function getLength<Value extends { length: number }>(
value: Value
): number {
return value.length;
}
TypeScriptここでは、
Value extends { length: number } が「前提条件」
getLength の中身が「その前提を使った処理」
という関係になっています。
このように、
「コードが暗黙に持っている前提条件を、extends で型に刻める」
なら、ジェネリクスにする価値があります。
セキュリティ的な前提条件を型にする
例えば、「id を持つものだけ扱う」関数。
function findByIdBad<T>(items: T[], id: number): T | undefined {
// @ts-ignore
return items.find((item) => item.id === id);
}
TypeScriptこれも、T に制約がないのに id を前提にしています。
ジェネリクス+制約で書き直すと、こうなります。
interface HasId {
id: number;
}
function findById<Entity extends HasId>(
items: Entity[],
id: number
): Entity | undefined {
return items.find((item) => item.id === id);
}
TypeScriptここでは、
「この関数は“id を持つエンティティ”だけを扱う」
というセキュリティ的にも重要な前提が、
型として明示されています。
判断基準として、
「この処理には“最低限こういう性質を持つ型しか渡してほしくない”という前提があるか?」
を考えてみてください。
あるなら、それはジェネリクス+制約の出番です。
判断基準5:「可読性と複雑さのバランスが取れているか」
ジェネリクスを入れたことで“読むのがつらくなっていないか”
どれだけ型が強くても、
誰も読めないコードは実務では負けです。
例えば、こういう型。
type Crazy<T, U, V> = Promise<Result<T & U, V | Error>>;
TypeScriptこれをその場でベタっと使うと、
読む人は一瞬で思考停止します。
こういうときは、意味のある名前を付けて分解します。
type Result<Value, Err> =
| { ok: true; value: Value }
| { ok: false; error: Err };
type AsyncResult<Value, Err> = Promise<Result<Value, Err>>;
TypeScriptこうしておけば、
function fetchUser(id: number): AsyncResult<User, "NotFound"> { ... }
TypeScriptと書けて、
「ユーザーを取ってきて、成功なら User、失敗なら ‘NotFound’ を返す非同期処理」
という意図が一瞬で伝わります。
判断基準として、
「このジェネリクス、コメントなしで読んで意味が分かるか?」
を自分に問いかけてください。
分からないなら、名前・分解・型エイリアス化を検討する価値があります。
型パラメータの数も“最小限”を意識する
例えば、こういう関数。
function transformBad<T, U, V>(
value: T,
fn1: (v: T) => U,
fn2: (v: U) => V
): V {
return fn2(fn1(value));
}
TypeScript動きは分かりますが、
T, U, V の関係を頭の中で追うのが少ししんどいです。
これを少し整理すると、こう書けます。
function pipe<Input, Middle, Output>(
value: Input,
fn1: (v: Input) => Middle,
fn2: (v: Middle) => Output
): Output {
return fn2(fn1(value));
}
TypeScript型パラメータの数は同じでも、
名前を変えるだけで可読性が大きく変わります。
「型パラメータの数は本当にこれだけ必要か?」
「名前だけで関係性が伝わるか?」
この二つは、ジェネリクス設計の最後のチェックポイントです。
まとめ:ジェネリクス設計の判断基準を自分の言葉で言うと
最後に、あなた自身の言葉でこう整理してみてください。
ジェネリクスにするかどうかを決める判断基準は、おおざっぱに言うと次のようなもの。
呼び出し側が“中身の型”を差し替えたいニーズがあるか
同じ“形”で中身だけ違う型が複数出てきているか
ジェネリクスにすることで any を減らせるか、型安全性が上がるか
コードが暗黙に持っている前提条件を、extends で型に刻めるか
そのジェネリクスを入れても、シグネチャが“読める範囲”に収まっているか
今書いている、あるいはこれから書こうとしている関数や型を一つ選んで、
この五つを順番に当てはめてみてください。
一つでも「はい」と強く言えるなら、
そこはジェネリクスにする価値がある場所です。
逆に、どれにも自信を持って「はい」と言えないなら、
その場はまだ具体型で書いておいて、
後から“パターンが見えたタイミングで抽象化する”くらいがちょうどいいです。
ジェネリクスは「全部に付ける魔法」ではなく、
「ここぞというところにだけ効かせる設計の道具」です。
その“ここぞ”を見極める目が、今の話の積み重ねで少しずつ育っていきます。

