[Vue 3] ジェネリックコンポーネント:タイプセーフで再利用性の高いUIコンポーネントを構築する

公開日 2025年5月10日
更新日 2025年5月10日
15 分で読める
Vue

現代のフロントエンド開発において、TypeScriptはその強力な型システムにより、大規模で堅牢なアプリケーションを構築するための強固な基盤を提供しています。

Vue 3.3で導入されたジェネリックコンポーネント(Generic Components)は重要なマイルストーンであり、TypeScriptの関数やクラスでジェネリクスを使用するのと同じように、Vueコンポーネントで型パラメータを定義・使用できるようになりました。これにより、コンポーネントの再利用性と型安全性が大幅に向上します。

1. ジェネリックコンポーネントとは?

TypeScriptに馴染みがあれば、ジェネリクス(Generics)についてはすでにご存知でしょう。ジェネリクスを使用すると、関数、インターフェース、またはクラスを定義する際に具体的な型を事前に指定せず、使用時に型を指定できます。

// TypeScriptにおけるジェネリック関数の例
function identity<T>(arg: T): T {
  return arg;
}

let output1 = identity<string>("hello"); // T は string として指定される
let output2 = identity<number>(123);    // T は number として指定される

Vue 3のジェネリックコンポーネントは、この概念を .vue 単一ファイルコンポーネント(SFC)に導入しました。<script setup> タグに generic 属性を追加することで、1つ以上の型パラメータを宣言し、props、emits、さらにはコンポーネント内部のロジックでこれらの型パラメータを使用できます。

2. ジェネリックコンポーネントの定義方法

Vue 3.3以降のバージョンでは、<script setup> 構文糖衣を使用してジェネリックコンポーネントを定義するのは非常に直感的です。以下では、より具体的な GenericTable コンポーネントを使用して、Vue 3のジェネリックコンポーネントの使用方法を説明します。このテーブルコンポーネントは、さまざまなタイプのデータとそれに対応する列定義を受け取り、テーブル形式で表示できます。

2.1 データ型の定義

export type User = {
  userId: string;
  userName: string;
  email: string;
  isActive: boolean;
};

export type Manager = {
  managerId: number;
  managerName: string;
  department: string;
  level: number;
};

2.2 ジェネリックテーブルコンポーネントの作成

このコンポーネントは、ジェネリック型 T をテーブル内の各行のデータ型として使用します。また、データをどのように表示するかを定義するための columns propも必要です。

<script setup lang="ts" generic="T extends { [key: string]: any }">
// generic="T" を使用してジェネリックパラメータ T を宣言
// extends { [key: string]: any } の追加は基本的な制約であり、T がオブジェクトであることを示します
// 必要に応じて、より具体的な制約を追加することもできます(例: T extends { id: string | number })

import type { VNode } from 'vue'; // render 関数が VNode を返す可能性がある場合

// 1. 列設定のインターフェースを定義し、これもジェネリクスを使用
// このインターフェースは、各列が T 型のオブジェクトからデータを抽出し表示する方法を記述します
export interface ColumnDefinition<ItemType> {
  key: keyof ItemType; // ポイント: keyof を使用して key が ItemType に存在することを保証
  header: string;       // ヘッダーに表示されるテキスト
  // オプションのカスタムレンダリング関数、最大限の柔軟性を提供
  render?: (item: ItemType) => VNode | string | number | boolean;
}

// 2. Props を定義し、ジェネリック T と ColumnDefinition<T> を使用
const props = defineProps<{
  items: T[];                             // テーブルデータ、T 型の配列
  columns: ColumnDefinition<T>[];         // 列定義の配列
  itemKey?: keyof T;                      // オプション: 行の :key に使用するプロパティ名を指定
}>();

// 3. ヘルパー関数: セルに表示する内容を取得
const getCellValue = (item: T, column: ColumnDefinition<T>) => {
  if (column.render) {
    // カスタムレンダリング関数がある場合はそれを使用
    return column.render(item);
  }
  // そうでなければ、直接プロパティ値にアクセス
  // Vue テンプレートはほとんどの基本型を自動的に処理します
  return item[column.key];
};

// 4. ヘルパー関数: 行の一意なキーを取得
const getRowKey = (item: T, index: number): string | number => {
  if (props.itemKey && (typeof item[props.itemKey] === 'string' || typeof item[props.itemKey] === 'number')) {
    // 有効な itemKey prop が提供され、対応する値が string または number の場合、それを使用します
     return item[props.itemKey] as string | number;
  }
  // そうでなければインデックスの使用にフォールバックします(一意のIDがない場合に必要ですが、一意のIDがある方が望ましいです)
  return index;
};

</script>

<template>
  <table class="generic-table">
    <thead>
      <tr>
        <th v-for="column in columns" :key="String(column.key)">
          {{ column.header }}
        </th>
      </tr>
    </thead>
    <tbody>
      <template v-if="items.length > 0">
        <tr v-for="(item, index) in items" :key="getRowKey(item, index)">
          <td v-for="column in columns" :key="String(column.key)">
            <span>{{ getCellValue(item, column) }}</span>
          </td>
        </tr>
      </template>
      <tr v-else>
        <td :colspan="columns.length || 1" class="no-data">
          データがありません
        </td>
      </tr>
    </tbody>
  </table>
</template>

2.3 ジェネリックテーブルコンポーネントの使用

これで、親コンポーネントで GenericTable を使用して User および Manager データを表示できます。

<script setup lang="ts">
import { ref } from 'vue';
import GenericTable, { type ColumnDefinition, type User, type Manager } from './GenericTable.vue'; // コンポーネントと型をインポート(User, Manager型もインポート元でexportされている前提)

// GenericTable.vue から User, Manager 型をエクスポートしていない場合は、ここで別途定義またはインポートが必要です
// 例:
// export type User = { userId: string; userName: string; email: string; isActive: boolean; };
// export type Manager = { managerId: number; managerName: string; department: string; level: number; };


type UserColumns = ColumnDefinition<User>[];
type ManagerColumns = ColumnDefinition<Manager>[];


// 1. データを準備
const users = ref<User[]>([
  { userId: 'USR001', userName: '張三', email: 'zhangsan@example.com', isActive: true },
  { userId: 'USR002', userName: '李四', email: 'lisi@example.com', isActive: false },
  { userId: 'USR003', userName: '王五', email: 'wangwu@example.com', isActive: true },
]);

const managers = ref<Manager[]>([
  { managerId: 101, managerName: '趙六', department: '技術部', level: 3 },
  { managerId: 102, managerName: '孫七', department: '市場部', level: 4 },
]);

// 2. User テーブルの列設定を定義
const userColumns = ref<UserColumns>([
  { key: 'userId', header: 'ユーザーID' },
  { key: 'userName', header: '氏名' },
  { key: 'email', header: 'メールアドレス' },
  {
    key: 'isActive',
    header: '状態',
    // カスタムレンダリング関数を使用
    render: (user) => user.isActive ? 'アクティブ' : '無効'
  }
]);

// 3. Manager テーブルの列設定を定義
const managerColumns = ref<ManagerColumns>([
  { key: 'managerId', header: 'マネージャーID' },
  { key: 'managerName', header: '氏名' },
  { key: 'department', header: '部門' },
  {
    key: 'level',
    header: 'レベル',
    render: (manager) => `Level ${manager.level}` // 簡単なフォーマット
  }
]);

</script>

<template>
  <div>
    <h2>ユーザーリスト</h2>
    <GenericTable
      :items="users"
      :columns="userColumns"
      itemKey="userId"
    />

    <hr style="margin: 2em 0;">

    <h2>マネージャーリスト</h2>
    <GenericTable
      :items="managers"
      :columns="managerColumns"
      itemKey="managerId"
    />
  </div>
</template>

3. まとめ

Vue 3のジェネリックコンポーネントは強力な機能であり、TypeScriptの型システムとVueのコンポーネントモデルをシームレスに統合し、タイプセーフで再利用性の高いUIコンポーネントの構築をかつてないほど簡単かつエレガントにします。Vue 3とTypeScriptを使用しており、より高品質で保守しやすいコードを目指しているのであれば、ジェネリックコンポーネントは間違いなく深く理解し、プロジェクトで実践する価値があります!

概要

技術的洞察、経験、思考を共有する個人ブログ

クイックリンク

お問い合わせ

  • Email: hushukang_blog@proton.me
  • GitHub

© 2025 CODE赤兎. 無断転載禁止