やあ、お疲れ。最近、君の書いているコードをレビューしていて「お、いい感じにコンポーネントの抽象化ができるようになってきたな」って感心してたところだ。
でもな、先日書いてくれた汎用的なリストコンポーネントのプルリクエスト、あれはちょっともったいなかった。`any` や `unknown` で型をガチガチに固めようとして、結果的に呼び出し側で `as` キャストの山になってたろ?あんなことやってると、TypeScriptを使っている意味が半分消えちまう。
中級から一段上のシニアへステップアップするなら、「ジェネリクス(Generics)」を使いこなせるかどうかが一つの分水嶺だ。今日は、ReactのコンポーネントPropsにジェネリクスを組み込み、型安全を一切妥協せずに「究極の再利用性」を手に入れる方法を伝授しよう。
—
なぜリストコンポーネントで型推論が狂うのか?
僕たちがよく作る「汎用リスト(List)」「ドロップダウン(Select)」「データグリッド(Table)」といったコンポーネントを思い出してほしい。これらは表示するデータの構造(型)が、使う場所によって全く異なる。
ここでよくあるアンチパターンがこれだ。
// ❌ やりがちなアンチパターン
type Props = {
items: any[];
renderItem: (item: any) => React.ReactNode;
};
これだと何が起きるか? `renderItem` の引数 `item` の型が `any` になり、内部でプロパティをドットつなぎで触った瞬間にTypeScriptの静的型チェックの恩恵がすべて消え去る。ブラウザのランタイムで「`undefined` のプロパティを読み取れません」というお馴染みのエラーに直面するわけだ。
じゃあ、これを `unknown` にしたところで、今度は呼び出し側で毎回型ガードを書くかアサーション(`as User`)で無理やり型を教え込まなきゃいけなくなる。これじゃ開発体験(DX)が最悪だ。
ここで登場するのが、TypeScriptのジェネリクス `
—
現場で即戦力になる!ジェネリクス型付きコンポーネントの実装
百聞は一見にしかず。実務でそのまま使える、型安全な汎用リストコンポーネント(`GenericList`)のコードを書いてみせる。エディタを開いて、この設計の美しさを感じてくれ。
import React from ‘react’;
// 1. 各アイテムが最低限持っていてほしい識別子(キー)の制約をかける
// どんなデータが来ようとも、ユニークなidがあるとリストレンダリングでkeyに指定できるから安心だ。
interface BaseItem {
id: string | number;
}
// 2. コンポーネントのPropsにジェネリクス
// ここで `extends BaseItem = BaseItem` のように制約(Constraints)をつけておくと、
// 予期しないプリミティブ型が渡されるのをコンパイル時に弾いてくれる。
export type GenericListProps
/ 表示するデータの配列 /
items: T[];
/ 1件ごとの要素を描画するレンダープロップ(ここで型 T が完璧に推論される) /
renderItem: (item: T, index: number) => React.ReactNode;
/ データが空の時に表示するフォールバック要素 /
emptyMessage?: string;
};
/
- どんなデータ構造でも受け入れられる、最高に柔軟で型安全なリストコンポーネント
/
export const GenericList =
items,
renderItem,
emptyMessage = “データがありません”,
}: GenericListProps
// ブラウザのレンダリング最適化を意識しつつ、itemsが空の場合のガードを挟む
if (items.length === 0) {
return
;
}
return (
-
{items.map((item, index) => (
- {renderItem(item, index)}
// Reactの再描画パフォーマンスの肝である key に安全にアクセスできる
))}
);
};
—
呼び出し側のコード:圧倒的な型推論の魔法
さて、この `GenericList` を実際の画面(例えばユーザー一覧と、タスク一覧)でどう使うかを見せよう。ここがジェネリクスの真骨頂だ。型を明示的に渡さなくても、TypeScriptのコンパイラが勝手に推論してくれる。
import React from ‘react’;
import { GenericList } from ‘./GenericList’;
// ユーザーデータの型定義
type User = {
id: number;
name: string;
email: string;
};
// タスクデータの型定義
type Task = {
id: string;
title: string;
completed: boolean;
};
const UserDashboard: React.FC = () => {
const users: User[] = [
{ id: 1, name: ‘Alice’, email: ‘alice@example.com’ },
{ id: 2, name: ‘Bob’, email: ‘bob@example.com’ },
];
const tasks: Task[] = [
{ id: ‘t-1’, title: ‘Reactの型設計を極める’, completed: false },
{ id: ‘t-2’, title: ‘PRレビューを終わらせる’, completed: true },
];
return (
ユーザー一覧
{/
ここでは T は自動的に User として推論される。
そのため、renderItem の引数 `user` は完全に User 型として補完され、
存在しないプロパティを叩こうものなら即座に赤線(コンパイルエラー)が出る。
/}
{user.name}
{user.email}
)}
/>
タスク一覧
{/
こちらでは、items に tasks を渡すだけで T が Task に自動スイッチする。
これぞジェネリクスの醍醐味だ。
/}
{task.title}
{task.completed ? ‘完了’ : ‘未完了’}
)}
/>
);
};
どうだ? 呼び出し側で `GenericList
開発者は「データを渡すだけ」で、IDEが勝手に型を安全に保ってくれるという最高の開発体験が手に入るんだ。
—
⚠️ アロー関数でジェネリクスを書くときの「実務での罠」
ここで、シニアとして君に一つだけ重要な実務上のハマりどころを共有しておこう。
Reactのコンポーネントをアロー関数で書くとき、TypeScriptのJSXパーサーとの兼ね合いで、時々コンパイラがジェネリクスを正しく解釈できずに構文エラーを起こすことがある。
例えば、こう書くとコンパイルエラーになることがある。
// ❌ JSXのタグと誤認されてコンパイルエラーになることがある
export const BadList =
これを回避するために、「extends にカンマ(`,`)を打ってジェネリクスであることをTypeScriptに明示する」というテクニックが実務ではよく使われる。
// ⭕️ カンマをつけることで、JSXのタグではなくジェネリクス(型パラメータ)であることをコンパイラに教える
export const GenericList =
// または今回の例のように:
export const GenericList =
もし今後、ジェネリクスを使ったコンポーネント定義で謎の構文エラーに遭遇したら、このカンマのテクニックを思い出してほしい。
—
まとめ
今日話した内容を振り返ろう。
1. `any` や `unknown` でごまかさない: 再利用性の高いコンポーネントこそジェネリクス `
2. 制約(Constraints)をかける: `T extends BaseItem` のように制約を設けることで、コンポーネント側で安全に共通プロパティ(`id` など)にアクセスできるようになる。
3. 推論を味方につける: 適切な型定義を行えば、呼び出し側でわざわざ型引数を渡さなくてもTypeScriptが賢く型を推論してくれる。
フロントエンドのアーキテクチャ設計において、「型を制約し、かつ柔軟性を保つ」というのは永遠のテーマだ。今回のジェネリクスをマスターすれば、君が作るUIライブラリや共通コンポーネントの信頼性は劇的に跳ね上がる。
さて、理論はここまでだ。早速、今君が手をつけている機能開発の共通モーダルやセレクトボックスのコードにこのジェネリクスを適用して、プルリクエストを投げてみてくれ。コードレビューでさらに深い話をしよう。期待してるぞ!

コメント