TypeScript | 関数・クラス・ジェネリクス:ジェネリクス基礎 - 可読性の高いジェネリクス

TypeScript TypeScript
スポンサーリンク

ゴール:「型が強いのに“怖くないコード”としてジェネリクスを書けるようになる」

ジェネリクスって、慣れてない人から見ると
「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;
}
TypeScript

Value 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);
}
TypeScript

Entity 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"> { ... }
TypeScript

Result<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 から「その役割を表す名前」に変えてみてください。

それだけで、

「ジェネリクスが“難しい記号”から、“意味のある日本語に近いもの”に変わる感覚」

が、少し掴めるはずです。

タイトルとURLをコピーしました