[Vue 3] ジェネリックコンポーネント:タイプセーフで再利用性の高いUIコンポーネントを構築する
現代のフロントエンド開発において、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を使用しており、より高品質で保守しやすいコードを目指しているのであれば、ジェネリックコンポーネントは間違いなく深く理解し、プロジェクトで実践する価値があります!