🚨

TypeScriptによる型安全なSelect

2024/04/21に公開

背景

とあるWebサイトにおいて、サイト内の設定によって表示を変えるような実装が必要な場合、ユーザに表示する部分(一般的にlabelと言われる)と内部で扱う固有の値(一般的にid, key, nameなど)を型安全に実装したいとします。
例として、多種多様な”性別”の分だけ項目がある場合のSelectの実装を考えます。

値固有のidを表現する

Facebookには58個の性別オプションがあるそうですが、わかりやすくするために、5種類程度に抑えて表現します。

const GenderBase = ["cisgender_female", "cisgender_male", "trans_female", "trans_male", "other"] as const;

ポイントはas constによりGenderBaseの型は値と等しいタプル型になります。

idを型として扱う

タプル型の値を型として扱うためには、Union型が適しています。

type Gender = typeof GenderBase[number];

これによって、”GenderBaseが持つ値のうちいずれか”という型を定義することができます。
例えば以下のコードはエラーになります。

// Gender型に"neither"は含まれないためエラーになります。
const gender: Gender = "neither";

labelを定義する。

labelの特徴は、idと1対1で対応している点です。これをデータとして表現する際には、前述のidの値すべてを漏れなく満たすよう注意する必要があります。

const GenderLabel = {
  cisgender_female: "女性",
  cisgender_male: "男性",
  trans_female: "性自認が女性の生物学的男性",
  trans_male: "性自認が男性の生物学的女性",
  other: "その他",
} as const satisfies Record<Gender, string>;

この実装のポイントはsatisfiesを使用することで、otherなどのキーが不足していたり、余分なキーが存在していたりする場合にエラーとなります。

活用

GenderBaseを配列として表現することで次のような活用方法や恩恵があります。

<Select
  option={GenderBase.map((value) => {
    return {
      name: value,
      label: GenderLabel[value]
    }
  })}
  {...props}
/>

上記のコードはSelect要素にoptionを追加する際の一例ですが、GenderBaseを参照することで、漏れなくoptionを追加することができます。
また、TypeScriptのオブジェクトの値は配列のように参照することができるため、GenderLabel["cisgender_female"]のようにすることで対になるlabelを参照することができます。
さらに、GenderLabelsatisfiesによって縛られているため、新しい性別を追加しようとした際に、GenderBaseの配列へ値を追加するだけで、修正が必要な箇所でコンパイルの段階でエラーとすることができます。

Discussion