ゴール:「型が強いのに“怖くないコード”としてジェネリクスを書けるようになる」
ジェネリクスって、慣れてない人から見ると
「T とか U とかよく分からん記号が飛び交ってて怖いコード」に見えがちです。
でも、本当にいいジェネリクスは逆で、
「読んだ瞬間に“ああ、こういう型の関係ね”と直感で分かる」
ものです。
ここでは、“強い型”と“読みやすさ”を両立させるジェネリクスの書き方を、
具体例を通して整理していきます。
型パラメータ名は「意味が伝わる最小限の長さ」にする
T だけで済むところと、ちゃんと名前を付けるべきところ
まず一番効くのが「型パラメータの名前」です。
例えば、これくらいシンプルな関数なら T で十分です。
function identity<T>(value: T): T {
return value;
}
TypeScript「value の型が T で、そのまま返すだけ」
ここにわざわざ長い名前は要りません。
一方で、複数の型パラメータが出てくるときは、意味のある名前を付けた方が圧倒的に読みやすくなります。
悪い例から見てみます。
function mapBad<T, U>(arr: T[], fn: (value: T) => U): U[] {
return arr.map(fn);
}
TypeScriptこれでも動くし、TypeScript 的には問題ありません。
でも、初心者が読むと「T と U の関係」が一瞬で頭に入ってきません。
同じ関数を、名前を変えて書き直してみます。
function mapArray<Input, Output>(
arr: Input[],
fn: (value: Input) => Output
): Output[] {
return arr.map(fn);
}
TypeScriptこれだけで、
「Input の配列を受け取って、Output の配列に変換する関数なんだな」
と、型パラメータ名だけで関係性が伝わるようになります。
ポイントは、
T で十分なところは T でいい
でも、2つ以上の型パラメータが絡むときは、Input / Output / Item / Result / Key / Value など、
“役割が分かる名前”にしてあげる
というバランスです。
セキュリティ視点の命名もアリ
例えば、認証・認可まわりの型なら、User, AuthenticatedUser, Payload, Token など、
ドメインに沿った名前を付けると、「何が安全で何が生のデータか」が読みやすくなります。
type ValidationResult<SafeValue> =
| { ok: true; value: SafeValue }
| { ok: false; error: string };
TypeScriptここで T ではなく SafeValue と書くことで、
「これは“検証済みの安全な値”なんだな」
という意図が、型パラメータ名から伝わります。
「左から右に読める」順番で型パラメータを並べる
関数のシグネチャは“ストーリー”として読めるようにする
可読性の高いジェネリクスは、シグネチャを左から右に読んだときにストーリーが通ります。
例えば、これは読みやすいです。
function mapArray<Input, Output>(
arr: Input[],
fn: (value: Input) => Output
): Output[] { ... }
TypeScript「Input の配列を受け取って、Input を Output に変換する関数を通し、Output の配列を返す」
という流れが、左から右に自然に読めます。
一方で、順番がぐちゃっとしていると読みにくくなります。
function mapWeird<Output, Input>(
arr: Input[],
fn: (value: Input) => Output
): Output[] { ... }
TypeScript動きは同じですが、
「Output, Input の順に出てくるのに、引数は Input から始まる」というズレが、
読む人の頭に余計な負荷をかけます。
基本ルールとして、
「呼び出し側から見て“入力 → 中間 → 出力”の順に型パラメータを並べる」
と覚えておくと、かなり読みやすくなります。
クラスや interface でも同じ
例えば、ジェネリックなリポジトリ。
interface Repository<Entity> {
add(entity: Entity): void;
findById(id: number): Entity | undefined;
}
TypeScriptここで Repository<T> より Repository<Entity> の方が、
「これは“何かのエンティティを扱うリポジトリ”なんだな」と伝わりやすいです。
もし ID の型もジェネリクスにしたいなら、こういう順番が自然です。
interface Repository<Entity, Id> {
add(entity: Entity): void;
findById(id: Id): Entity | undefined;
}
TypeScript「何を扱うか(Entity)」→「それを識別するもの(Id)」
という順番になっているので、読みやすさが保たれます。
制約(extends)は「何を期待しているか」をはっきり書く
T extends 〜 は“前提条件”を明文化する
制約付きジェネリクスは、「この T は最低限こういう性質を持っている」という前提条件です。
例えば、これは読みやすい制約です。
function getLength<Value extends { length: number }>(
value: Value
): number {
return value.length;
}
TypeScriptValue extends { length: number } と書くことで、
「Value は length プロパティを持つ何かだよ」
という前提が一目で分かります。
これを省略してしまうと、
「なんで length がある前提で書いてるの?」と読む人が混乱します。
制約は、
「このジェネリクスは、何でもアリじゃないよ。
最低限、こういう性質は持っていてね」
という“契約書”です。
契約は、読めば分かる形で書いておくほど可読性が上がります。
セキュリティ的な制約もかなり有効
例えば、「必ず id を持つものだけ扱いたい」ならこう書けます。
interface HasId {
id: number;
}
function findById<Entity extends HasId>(
entities: Entity[],
id: number
): Entity | undefined {
return entities.find((e) => e.id === id);
}
TypeScriptEntity extends HasId と書いておくことで、
「この関数は“id を持つエンティティ”だけを対象にしている」
という意図が、型から伝わります。
これは、「ID を持たないものをうっかり渡してしまう」というバグを防ぐだけでなく、
「ID をキーにしたアクセス制御や監査を前提にしている」ことも読み手に伝えます。
「型エイリアス+ジェネリクス」で意味を名前に閉じ込める
生のジェネリクスより「名前付きの型」の方が読みやすい
例えば、こういう関数があったとします。
function handleResponse<T>(
res: { success: boolean; data: T | null; errorMessage?: string }
): T | null {
if (!res.success) return null;
return res.data;
}
TypeScriptこれでも動きますが、
オブジェクト型がその場にベタっと書かれていて、少し読みにくいです。
ここで、型エイリアスを使って名前を付けてあげます。
type ApiResponse<Data> = {
success: boolean;
data: Data | null;
errorMessage?: string;
};
function handleResponse<Data>(res: ApiResponse<Data>): Data | null {
if (!res.success) return null;
return res.data;
}
TypeScriptこれだけで、
「handleResponse は ApiResponse<Data> を受け取って、Data | null を返す関数」
という構造が、一瞬で頭に入ってくるようになります。
“その場で複雑なジェネリック型を書く”のではなく、
「意味のある名前を付けたジェネリック型エイリアスを定義して、それを使う」
というスタイルは、可読性をかなり上げてくれます。
Result<T, E> みたいな「お決まりパターン」を作る
例えば、エラーを含む結果型。
type Result<Value, Error> =
| { ok: true; value: Value }
| { ok: false; error: Error };
TypeScriptこれを使うと、関数のシグネチャが一気に読みやすくなります。
function parseEmail(input: string): Result<string, "InvalidEmail"> { ... }
function parseAge(input: string): Result<number, "InvalidAge"> { ... }
TypeScriptResult<Value, Error> という名前だけで、
「成功か失敗かのどちらかで、成功なら Value、失敗なら Error」
という構造が伝わります。
「よく出てくるジェネリックな型パターンには、名前を付けてあげる」
これも可読性の高いジェネリクスの重要なテクニックです。
「コメントしなくても意図が伝わるか?」を最後のチェックにする
コメントが必要なジェネリクスは、だいたいまだ“生煮え”
例えば、こんなコードがあったとします。
// T: input type, U: output type
function transform<T, U>(value: T, fn: (v: T) => U): U {
return fn(value);
}
TypeScriptコメントで説明している時点で、
「型パラメータ名だけでは意図が伝わっていない」というサインです。
これをこう直せます。
function transform<Input, Output>(
value: Input,
fn: (v: Input) => Output
): Output {
return fn(value);
}
TypeScriptもうコメントはいりません。
シグネチャそのものが説明になっています。
可読性の高いジェネリクスかどうかを判断するとき、
自分にこう問いかけてみてください。
「この <T, U> に、コメントを書かないと意味が伝わらないなら、
名前や構造を見直す余地があるかも。」
コメントで補う前に、
型パラメータ名・順番・型エイリアス化・制約を見直してみると、
だいたいコードそのものが説明になってくれます。
まとめ:可読性の高いジェネリクスを自分の言葉で説明すると
最後に、あなた自身の言葉でこう整理してみてください。
可読性の高いジェネリクスとは、
「型パラメータ名・順番・制約・型エイリアスの使い方によって、
シグネチャを“読むだけで意図が分かる”ようにしたジェネリクス」。
そのために意識するのは、
型パラメータ名は役割が分かるように(Input / Output / Entity / Id など)
複数あるときは“入力 → 出力”の順に並べる
制約(extends)は“前提条件”としてはっきり書く
複雑な型はジェネリック型エイリアスに切り出して名前を付ける
コメントがなくても、シグネチャだけで関係性が読めるかをチェックする
今書いているジェネリクスを一つ選んで、
型パラメータ名を T から「その役割を表す名前」に変えてみてください。
それだけで、
「ジェネリクスが“難しい記号”から、“意味のある日本語に近いもの”に変わる感覚」
が、少し掴めるはずです。
