TypeScript + React で <select>
の <option>
を型安全に定義する最適な方法
はじめに
React で <select>
を使うとき、<option>
に設定できる値を型安全に管理する方法を考えたことはありますか?
通常、<option>
の値は string
型で扱われますが、選択肢を増やしたり変更したりするときに、意図しない値が設定されるリスク があります。
そこで、試していたTypeScript の enum
と Record
を組み合わせて、型安全な <select>
の実装を作る方法 を紹介したいと思います!
今回は、フルーツの選択肢(りんご・バナナ・オレンジ・ぶどう)を例に、型安全な <select>
の実装方法を解説します。
フルーツの選択肢を型安全に定義しよう!🍎🍌🍊🍇
通常の方法では、次のように手動で <option>
を定義します。
<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
を使って、選択肢の値(フルーツの英語名)を固定します。
enum FruitEnum {
Apple = 'apple',
Banana = 'banana',
Orange = 'orange',
Grape = 'grape',
}
✅ FruitEnum
を使うことで、選択肢の値が厳密に定義され、value
の間違いを防ぐことができます!
🎯 Record
を使って option
の表示テキストを管理
次に、各フルーツに対応する 表示テキスト を Record<FruitEnum, Fruit>
を使って定義します。
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>
を動的に生成できます。
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>
を作成できます! 🎉
いかがでしょう。ご意見があれば、ぜひお聞かせください!