0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

はじめに

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

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

そこで、試していたTypeScript の enumRecord を組み合わせて、型安全な <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 の enumRecord を活用して、安全な <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> を作成できます! 🎉

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?