0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScriptのジェネリクスを使ってみた振り返り

Posted at

やりたかったこと

  • 目的: 異なるデータ型(ユーザー、商品、注文など)を扱う汎用的なデータ変換関数を作り、型安全に再利用。
  • 要件:
    • 複数のデータ型に対応(例: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 }で、入力データがidnameプロパティを持つことを保証。
  • 再利用性: UserProductも同じ関数で処理。型エラーが出ない。
  • シンプルな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でデータ型を動的に指定。
  • 型安全なレンダリング: renderItemT型を受け取り、型エラーを防止。
  • 再利用性: 1つのコンポーネントでUserProductも表示。

ハマったポイントと対策

ジェネリクスは強力だけど、最初は型エラーでハマった。よくあるパターンと対処法をまとめる。

  1. 型制約の不足
    エラー例: 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で必要なプロパティを指定。

  2. 型推論の失敗
    エラー例: 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>で明示的に指定。

  3. 複雑なジェネリクスの型エラー
    エラー例: 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のホバーで確認。

  4. 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で型を絞る。

使ってみて良かった点

  • コードの再利用性: ジェネリクスで1つの関数がUser, Product, Orderに対応。コード量が減った。
  • 型安全性: 型エラーをコンパイル時にキャッチでき、バグが減った。
  • チームでの使いやすさ: シンプルなAPIで、チームメンバーが型を意識せずに使える。
  • VS Codeの支援: ジェネリクスの型推論をホバーで確認でき、デバッグが楽。

まとめ

TypeScriptのジェネリクスを使って、汎用的なデータ変換ユーティリティを作ってみた。異なるデータ型を1つの関数で扱えるようになり、コードの再利用性と型安全性が上がった。型制約や型推論でハマったけど、extendsや明示的な型指定で解決。Reactコンポーネントでもジェネリクスを活用でき、全体的な開発効率も向上した。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?