TypeScriptのRecord型で実現する柔軟なUI: 動的検索フォームと可変列テーブルの実装

# はじめに
フロントエンド開発において、ユーザーの要求に応じて動的に変化するUIは非常に重要です。特に、検索フォームやデータテーブルのような複雑なコンポーネントでは、柔軟性と型安全性の両立が求められます。TypeScriptのRecord型を活用することで、これらの課題を効果的に解決できます。

この記事では、以下の2つのシナリオに焦点を当て、Record型を使った実装方法を詳しく解説します。

1. 動的に検索項目を増減できる検索フォーム
2. 表示する列が可変なデータテーブル

:::note info
**TypeScriptのRecord型とは？**

Record型は、オブジェクトのキーと値の型を指定するためのユーティリティ型です。`Record<K, T>`の形式で使用し、KがキーのType、Tが値のTypeを表します。これにより、動的なオブジェクト構造を型安全に扱うことができます。
:::

:::note warn
この記事のサンプルコードはVue 3のComposition API（`<script setup>`構文）を使用しています。
:::

## 動的検索フォームの実装

### 基本的な構造

まず、検索項目を表現するための型を定義します：

```typescript
type FieldType = 'text' | 'number' | 'date'

// フィールドの定義
interface FieldDefinition {
  key: string
  label: string
  type: FieldType
}

// 検索フォームの値
type SearchFormValues = Record<string, string | number | Date>

// フィールド定義（通常はAPIから取得）
const fieldDefinitions = ref<FieldDefinition[]>([
  { key: 'name', label: '名前', type: 'text' },
  { key: 'age', label: '年齢', type: 'number' },
  { key: 'birthdate', label: '生年月日', type: 'date' },
  { key: 'email', label: 'メールアドレス', type: 'text' },
])
```

この構造により、検索フィールドを動的に追加・削除できるようになります。

### 検索フォームコンポーネント（Vue 3 Composition API）

```vue
<script lang="ts" setup>

const searchValues = ref<SearchFormValues>({
  name: '',
})

// 選択中のフィールドキー
const activeFields = computed(() => Object.keys(searchValues.value))

// 追加可能なフィールド
const availableFields = computed(() =>
  fieldDefinitions.value.filter(
    (field) => !activeFields.value.includes(field.key),
  ),
)

// フィールド定義を取得するヘルパー関数
const getFieldDefinition = (key: string) =>
  fieldDefinitions.value.find((field) => field.key === key)

const addField = (event: Event) => {
  const target = event.target as HTMLSelectElement
  const selectedKey = target.value
  const field = getFieldDefinition(selectedKey)
  if (field) {
    searchValues.value[selectedKey] = field.type === 'number' ? 0 : ''
  }
}

const removeField = (key: string) => {
  const { [key]: _, ...rest } = searchValues.value
  searchValues.value = rest
}

const handleSubmit = () => {
  console.log('検索条件:', searchValues.value)
  // ここで検索ロジックを実装
}
</script>

<template>
  <div>
    <form @submit.prevent="handleSubmit">
      <div v-for="key in activeFields" :key="key">
        <label :for="key">{{ getFieldDefinition(key)?.label }}</label>
        <input
          :id="key"
          v-model="searchValues[key]"
          :type="getFieldDefinition(key)?.type"
        />
        <button type="button" @click="removeField(key)">削除</button>
      </div>

      <div v-if="availableFields.length">
        <label for="field-select">フィールド追加:</label>
        <select id="field-select" @change="addField">
          <option value="">選択してください</option>
          <option
            v-for="field in availableFields"
            :key="field.key"
            :value="field.key"
          >
            {{ field.label }}
          </option>
        </select>
      </div>

      <button type="submit">検索</button>
    </form>
  </div>
</template>


```

このコンポーネントでは、`Record`型を使用して動的なフィールドを管理しています。ユーザーは自由にフィールドを追加・削除でき、各フィールドの型も適切に管理されます。

## 可変列テーブルの実装

### テーブルの型定義

```typescript
interface ColumnDefinition {
  key: string
  label: string
}

// プロジェクトの行データ型
type ProjectData = {
  id: number // idは固定
} & Record<string, string | number | Date>

// これを追加削除したら列が可変になる
const columns: ColumnDefinition[] = [
  { key: 'projectName', label: 'プロジェクト名' },
  { key: 'deadline', label: '期日' },
  { key: 'assignee', label: '担当者' },
  { key: 'status', label: 'ステータス' },
  { key: 'estimatedHours', label: '予定工数' },
  { key: 'progress', label: '進捗率' },
  { key: 'priority', label: '優先度' },
]

```

この構造により、列の定義と各行のデータを柔軟に管理できます。

### テーブルコンポーネント（Vue 3 Composition API）

```vue
<script lang="ts" setup>

// 渡って来なかった項目は値が表示されない
const projects: ProjectData[] = [
  {
    id: 1,
    projectName: '新規ECサイト構築',
    deadline: '2024-03-31',
    assignee: '山田太郎',
    status: '進行中',
    estimatedHours: 160,
    progress: 35,
    priority: '高',
  },
  {
    id: 2,
    projectName: 'レガシーシステム改修',
    deadline: '2024-06-30',
    assignee: '鈴木花子',
    status: '計画中',
    estimatedHours: 240,
    priority: '中',
  },
  {
    id: 3,
    projectName: 'モバイルアプリUI改善',
    assignee: '田中一郎',
    status: 'レビュー中',
    estimatedHours: 80,
    progress: 90,
    priority: '高',
  },
]

</script>

<template>
  <table>
    <thead>
      <tr>
        <th v-for="col in columns" :key="col.key">
          {{ col.label }}
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="project in projects" :key="project.id">
        <td v-for="col in columns" :key="col.key">
          {{ project[col.key] }}
        </td>
      </tr>
    </tbody>
  </table>
</template>


```

このコンポーネントでは、`columns`プロパティを使用して動的に列を生成し、`data`の各要素を`Record`型として扱うことで、柔軟なデータ表示を実現しています。
これにより、DBに項目名を持たせることができ自由度高く項目の追加/削除が実現できる。

## まとめ

TypeScriptのRecord型を活用することで、動的な検索フォームや可変列テーブルといった柔軟なUIコンポーネントを型安全に実装できます。これにより、以下のメリットが得られます：

1. コードの可読性と保守性の向上
2. 実行時エラーの減少
3. 開発効率の向上

Record型の特性を理解し、適切に活用することで、より柔軟で堅牢なフロントエンド開発が可能になります。ぜひ、自身のプロジェクトでこれらのテクニックを試してみてください。

## 参考資料

1. [TypeScript公式ドキュメント - Utility Types](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type)