TypeScript + React で `<select>` の `<option>` を型安全に定義する方法を試してみた

# TypeScript + React で `<select>` の `<option>` を型安全に定義する最適な方法

## はじめに

React で `<select>` を使うとき、`<option>` に設定できる値を型安全に管理する方法を考えたことはありますか？

通常、`<option>` の値は `string` 型で扱われますが、選択肢を増やしたり変更したりするときに、**意図しない値が設定されるリスク** があります。

そこで、試していた**TypeScript の `enum` と `Record` を組み合わせて、型安全な `<select>` の実装を作る方法** を紹介したいと思います！

今回は、フルーツの選択肢（りんご・バナナ・オレンジ・ぶどう）を例に、型安全な `<select>` の実装方法を解説します。

## フルーツの選択肢を型安全に定義しよう！🍎🍌🍊🍇

通常の方法では、次のように手動で `<option>` を定義します。

```tsx
<select>
  <option value="apple">りんご</option>
  <option value="banana">バナナ</option>
  <option value="orange">オレンジ</option>
  <option value="grape">ぶどう</option>
</select>
```

### ⚠️ 問題点

- `value` が自由な `string` なので、スペルミス (`"bananna" → "banana"` など) に気づきにくい
- 新しい選択肢を追加するとき、間違いを防ぐ方法がない
- 選択肢と関連するデータ（例: 価格）が分離していて、連携しづらい

そこで、**TypeScript の `enum` と `Record` を活用して、安全な `<select>` の実装を作ります！** 🎯

---

## 型安全な `<select>` の実装方法

### 🎯 `enum` を使って選択肢の値を定義

まず、`enum` を使って、選択肢の値（フルーツの英語名）を固定します。

```typescript
enum FruitEnum {
  Apple = 'apple',
  Banana = 'banana',
  Orange = 'orange',
  Grape = 'grape',
}
```

**✅ `FruitEnum` を使うことで、選択肢の値が厳密に定義され、`value` の間違いを防ぐことができます！**

---

### 🎯 `Record` を使って `option` の表示テキストを管理

次に、各フルーツに対応する **表示テキスト** を `Record<FruitEnum, Fruit>` を使って定義します。

```typescript
type Fruit = {
  name: string;
  price: number;
};

const fruitOptions: Readonly<Record<FruitEnum, Fruit>> = {
  [FruitEnum.Apple]: { name: 'りんご 🍎', price: 100 },
  [FruitEnum.Banana]: { name: 'バナナ 🍌', price: 50 },
  [FruitEnum.Orange]: { name: 'オレンジ 🍊', price: 80 },
  [FruitEnum.Grape]: { name: 'ぶどう 🍇', price: 200 },
};
```

**✅ `Record<FruitEnum, Fruit>` を使うことで、すべての `FruitEnum` の値が `fruitOptions` に存在しなければならないルールを TypeScript が保証！**

また、**選択肢とその情報（たとえば価格）を簡単に連携できる**こともメリットです。

---

### 🎯 `map()` を使って `<option>` を動的に生成

ここまでの準備ができたら、`map()` を使って `<option>` を動的に生成できます。

```tsx
import React, { useState } from 'react';

const FruitSelector = () => {
  const [selectedFruit, setSelectedFruit] = useState<FruitEnum | ''>('');

  return (
    <div>
      <select
        value={selectedFruit}
        onChange={(e) => setSelectedFruit(e.target.value as FruitEnum)}
      >
        <option value="" disabled>
          フルーツを選んでください
        </option>
        {Object.values(FruitEnum).map((key) => (
          <option key={key} value={key}>
            {fruitOptions[key].name}
          </option>
        ))}
      </select>

      {/* フルーツが選択されているなら、price を表示 */}
      {selectedFruit && (
        <p>
          選んだ果物の値段:
          <strong>{fruitOptions[selectedFruit].price}</strong>円
        </p>
      )}
    </div>
  );
};

export default FruitSelector;
```

### ✅ メリット

- `Object.values(FruitEnum).map()` を使うことで、選択肢を **自動的に `<option>` に変換** できる！
- **新しい `FruitEnum` を追加すると、`fruitOptions` にも追加しないと TypeScript がエラーを出すので、ミスを防げる！**
- **型安全な `setSelectedFruit(e.target.value as FruitEnum)` により、選択値が常に `FruitEnum` になる！**
- **`fruitOptions` により、選択肢とその情報（ここでは価格）を簡単に連携できる！**

---

## まとめ

- `enum` を使うと、フルーツの種類など「固定の選択肢」を型で管理できる。
- `Record<FruitEnum, Fruit> + Readonly<...>` で `Enum` の各値に対応するデータを不変オブジェクトとして保持し、網羅性と安全性を向上。
- `Object.values(Enum)` を使って `<option>` を一括生成し、漏れや余計な値を防ぐ。
- 型定義とコンパイラが「抜け・不整合・誤変更」を検知するため、保守性や変更への強さが大幅に向上。

この方法を使うことで、**型安全かつメンテナブルな `<select>` を作成できます！** 🎉

いかがでしょう。ご意見があれば、ぜひお聞かせください！