やりたかったこと
- 目的: 異なるデータ型(ユーザー、商品、注文など)を扱う汎用的なデータ変換関数を作り、型安全に再利用。
-
要件:
- 複数のデータ型に対応(例:
User
,Product
,Order
)。 - 型エラーを防ぎつつ、コードの重複を最小化。
- チームが使いやすいシンプルなAPI。
- 複数のデータ型に対応(例:
- なぜジェネリクス?: 型を固定せず、呼び出し側で型を指定できるので、柔軟性と型安全性を両立できる。プロジェクトのユーティリティ関数に最適だった。
実装の流れ
プロジェクトでは、APIから取得したデータをフロントエンドで表示用に変換するユーティリティを作った。たとえば、ユーザーや商品のデータを特定の形式(例:IDと表示名のペア)に変換する処理を汎用化。TypeScriptのジェネリクスを使って実装した。
1. 基本的なジェネリクス関数の実装
APIから取得したデータを、表示用の{ id, displayName }
形式に変換する関数を作った。ジェネリクスを使って、異なるデータ型に対応。
interface DisplayItem {
id: number;
displayName: string;
}
// ジェネリクスで汎用的な変換関数
function transformToDisplayItem<T extends { id: number; name: string }>(
items: T[]
): DisplayItem[] {
return items.map(item => ({
id: item.id,
displayName: item.name,
}));
}
// 使用例
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
name: string;
price: number;
}
const users: User[] = [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" },
];
const products: Product[] = [
{ id: 101, name: "Laptop", price: 1000 },
{ id: 102, name: "Phone", price: 500 },
];
const userDisplayItems = transformToDisplayItem(users);
const productDisplayItems = transformToDisplayItem(products);
console.log(userDisplayItems); // [{ id: 1, displayName: "Alice" }, { id: 2, displayName: "Bob" }]
console.log(productDisplayItems); // [{ id: 101, displayName: "Laptop" }, { id: 102, displayName: "Phone" }]
ポイント
-
型制約 (
extends
):T extends { id: number; name: string }
で、入力データがid
とname
プロパティを持つことを保証。 -
再利用性:
User
もProduct
も同じ関数で処理。型エラーが出ない。 - シンプルなAPI: チームが「型を考えず」に呼び出せる。
2. 複雑なデータ変換でのジェネリクス
次に、データにカスタムプロパティを追加するユーティリティを実装。たとえば、データをグループ化して集計する関数を作った。
interface GroupedResult<K extends string, T> {
groupKey: K;
items: T[];
count: number;
}
function groupByKey<T extends { id: number }, K extends string>(
items: T[],
keySelector: (item: T) => K
): GroupedResult<K, T>[] {
const map = new Map<K, T[]>();
items.forEach(item => {
const key = keySelector(item);
const group = map.get(key) || [];
group.push(item);
map.set(key, group);
});
return Array.from(map, ([groupKey, items]) => ({
groupKey,
items,
count: items.length,
}));
}
// 使用例
interface Order {
id: number;
category: string;
amount: number;
}
const orders: Order[] = [
{ id: 1, category: "Electronics", amount: 100 },
{ id: 2, category: "Clothing", amount: 50 },
{ id: 3, category: "Electronics", amount: 200 },
];
const groupedOrders = groupByKey(orders, order => order.category);
console.log(groupedOrders);
// [
// { groupKey: "Electronics", items: [{ id: 1, ...}, { id: 3, ...}], count: 2 },
// { groupKey: "Clothing", items: [{ id: 2, ...}], count: 1 }
// ]
ポイント
-
複数ジェネリクス:
T
(データ型)とK
(グループキーの型)を別々に定義。 -
型制約:
K extends string
でグループキーが文字列であることを保証。 -
型安全:
keySelector
の戻り値がK
に一致し、型エラーを防ぐ。
3. Reactコンポーネントでのジェネリクス
Reactで汎用的なテーブルコンポーネントを作り、異なるデータ型を表示。ジェネリクスで型安全に。
interface TableProps<T> {
data: T[];
renderItem: (item: T) => JSX.Element;
}
function GenericTable<T>({ data, renderItem }: TableProps<T>) {
return (
<table>
<tbody>
{data.map((item, index) => (
<tr key={index}>{renderItem(item)}</tr>
))}
</tbody>
</table>
);
}
// 使用例
const userTable = (
<GenericTable
data={users}
renderItem={(user: User) => (
<>
<td>{user.id}</td>
<td>{user.name}</td>
<td>{user.email}</td>
</>
)}
/>
);
const productTable = (
<GenericTable
data={products}
renderItem={(product: Product) => (
<>
<td>{product.id}</td>
<td>{product.name}</td>
<td>${product.price}</td>
</>
)}
/>
);
ポイント
-
ジェネリクスで柔軟性:
T
でデータ型を動的に指定。 -
型安全なレンダリング:
renderItem
がT
型を受け取り、型エラーを防止。 -
再利用性: 1つのコンポーネントで
User
もProduct
も表示。
ハマったポイントと対策
ジェネリクスは強力だけど、最初は型エラーでハマった。よくあるパターンと対処法をまとめる。
-
型制約の不足
エラー例:TS2339: Property 'name' does not exist on type 'T'.
ジェネリクスT
にプロパティを仮定してアクセスするとエラー。再現コード:
function getName<T>(item: T) { return item.name; // エラー }
対処:
-
extends
で型を制限。
function getName<T extends { name: string }>(item: T) { return item.name; // OK }
反射的アクション: プロパティエラーなら、
extends
で必要なプロパティを指定。 -
-
型推論の失敗
エラー例:TS2345: Argument of type 'xxx' is not assignable to parameter of type 'T'.
ジェネリクスの型推論が期待通りでない場合。再現コード:
function wrap<T>(value: T) { return [value]; } const result = wrap({ name: "Alice" }); // Tが{ name: string }と推論されるが、意図しない場合も
対処:
- 明示的に型を指定。
const result = wrap<{ name: string; age: number }>({ name: "Alice" }); // OK
反射的アクション: 型推論がズレたら、
<Type>
で明示的に指定。 -
複雑なジェネリクスの型エラー
エラー例:TS2322: Type 'xxx' is not assignable to type 'T'.
複数ジェネリクスや関数型でハマる。再現コード:
function mapTo<T, K>(items: T[], mapper: (item: T) => K) { return items.map(mapper); } const names = mapTo(users, user => user.email); // エラー: Type 'string' is not assignable to type 'K'.
対処:
- 型制約を追加し、戻り値を明示。
function mapTo<T, K extends string | number>(items: T[], mapper: (item: T) => K) { return items.map(mapper); // OK }
反射的アクション: 複雑なジェネリクスなら、
extends
で型を絞り、VS Codeのホバーで確認。 -
Reactコンポーネントのジェネリクス
エラー例:TS2322: Type '{}' is not assignable to type 'T'.
Reactコンポーネントでジェネリクスを使うと、Propsの型推論が難しい。再現コード:
function MyComponent<T>({ data }: { data: T }) { return <div>{data.name}</div>; // エラー: Property 'name' does not exist on type 'T'. }
対処:
- Propsにインターフェースを定義し、
extends
で制限。
interface MyProps<T extends { name: string }> { data: T; } function MyComponent<T extends { name: string }>({ data }: MyProps<T>) { return <div>{data.name}</div>; // OK }
反射的アクション: Reactのジェネリクスは、Propsインターフェースを定義して
extends
で型を絞る。 - Propsにインターフェースを定義し、
使ってみて良かった点
-
コードの再利用性: ジェネリクスで1つの関数が
User
,Product
,Order
に対応。コード量が減った。 - 型安全性: 型エラーをコンパイル時にキャッチでき、バグが減った。
- チームでの使いやすさ: シンプルなAPIで、チームメンバーが型を意識せずに使える。
- VS Codeの支援: ジェネリクスの型推論をホバーで確認でき、デバッグが楽。
まとめ
TypeScriptのジェネリクスを使って、汎用的なデータ変換ユーティリティを作ってみた。異なるデータ型を1つの関数で扱えるようになり、コードの再利用性と型安全性が上がった。型制約や型推論でハマったけど、extends
や明示的な型指定で解決。Reactコンポーネントでもジェネリクスを活用でき、全体的な開発効率も向上した。