【React】string型に甘えずselectフィールドの型をType Guardで縛ってみる

React
  • URLをコピーしました!

こんにちは!RYOTAです!

当記事をご覧いただきありがとうございます!

React × TypeScriptでselectフィールドの型制御を行なった際の備忘録として、実装方法を簡単にまとめた記事となります。

目次

はじめに

タイトル通りとなりますが、React で select フィールドの型を縛ってみました。

結論から言うとType Guardを挟むことで特定の値しか受け付けず、string型の許容を排除した内容となります。

なんて事のない、ただのselectフィールドがこちら

const [selectValue, setSelectValue] = useState('hoge')

<select value={selectValue} onChange={(e) => setSelectValue(e.target.value)}>
  <option value="hoge">hoge</option>
  <option value="fuga">fuga</option>
  <option value="piyo">piyo</option>
</select>

要はこのselectValuehoge / fuga / piyo 以外の値が入らないように型を縛ったということですね。

上記では分かりやすくoptionを並べましたが、一旦optionsを定数化してmapで並べる一般的な形に変換しましょう。

(通常この手の定数はconstants等にまとめますが、今回はわかりやすく同一ファイルに書いています。)

const SELECT_OPTIONS = ['hoge', 'fuga', 'piyo'] as const

const [selectValue, setSelectValue] = useState('hoge')

<select value={selectValue} onChange={(e) => setSelectValue(e.target.value)}>
  {SELECT_OPTIONS.map((v) => (
    <option key={v} value={v}>{v}</option>
  ))}
</select>

ではこの状態から始めましょう。

第一段階: useStateの型を縛る

まずはSELECT_OPTIONSのリストからユニオン型のTSelectFieldを生成しておきます。

const SELECT_OPTIONS = ['hoge', 'fuga', 'piyo'] as const
type TSelectField = typeof SELECT_OPTIONS[number]
// -> type TSelectField = "hoge" | "fuga" | "piyo"

そんでもってこのTSelectFielduseStateに割り当ててあげます。

そうすると当然ですがselectValueが”hoge” | “fuga” | “piyo”に型補完されてますね。(そりゃそうじゃ)

const [selectValue, setSelectValue] = useState<TSelectField>('hoge')
// selectValueをホバー
// -> const selectValue: "hoge" | "fuga" | "piyo"

ここまでは何の変哲もないuseStateの扱い方ですが、一点問題点が発生しています。

はいエラってますね。

そうです。useState<TSelectField>で型を縛ったことによって、selectのonChage Eventとの型にズレが生じています。

こいつを解決するのが今回の主目的です。

第二段階: 型アサーションで強引に割り当てる

手取り早く、型アサーション( as )で無理やり型を割り当ててあげます。

// as TSelectField 追加
<select value={selectValue} onChange={(e) => setSelectValue(e.target.value as TSelectField)}>
  {SELECT_OPTIONS.map((v) => (
    <option key={v} value={v}>{v}</option>
  ))}
</select>

これでエラーも消えて解決!!!

・ 

・ 

否!そうは問屋が卸しません!!

asを使うとコンパイルエラーを握りつぶすことが出来ますが、実行時エラーを出すケースがあります。

as はコンパイラの型推論を強制的に上書いているに過ぎないので、注意しないと予期せぬバグを孕む危険性があります。

実際に私が現在参画している案件では as の使用を固く禁じています。

as の危険性については調べると山ほど記事が出てくるので是非ご自身でも調べてみてください。

第三段階: Type Guard を仕込んでみる

お待たせしました。ここからようやく本題です。笑

やることは非常にシンプルです。

e.target.valueがTSelectField (“hoge” | “fuga” | “piyo”)とマッチしているかを事前にチェックすることで、TSelectField以外のstring型が入る余地を無くしてあげれば良いのです。

実はselectのeventの型ChangeEvent<HTMLSelectElement>は変更が出来ないので、stringとして受け取ってType Guardでチェックする関数を定義します。

const onChangeSelect = (event: ChangeEvent<HTMLSelectElement>) => {
  // as を使わず Type Guard で 型チェックする
  setSelectValue(event.target.value as TSelectField)
}

<select value={selectValue} onChange={onChangeSelect}>
  {SELECT_OPTIONS.map((v) => (
    <option key={v} value={v}>{v}</option>
  ))}
</select>

ここまで出来たら後はsetSelectValueの前でType Guardしてあげるだけです。

const isTSelectField = (value: string): value is TSelectField =>
  Object.values(SELECT_OPTIONS)
    .map<string>((value) => value)
    .includes(value)

const onChangeSelect = (event: ChangeEvent<HTMLSelectElement>) => {
  // Type Guardを追加
  if (isTSelectField(event.target.value)) setSelectValue(event.target.value)
  // 後ろのvalueをホバー: -> (property) HTMLSelectElement.value: "hoge" | "fuga" | "piyo"
}

おりゃ!

asを使っていませんがevent.target.valueの方が”hoge” | “fuga” | “piyo”に補完されてますね!

isTSelectFieldを分解すると

// SELECT_OPTIONS から string配列型 の新たな配列を生成
// 新しい配列を生成することでエラーを回避 (型 'string' の引数を型 '"hoge" | "fuga" | "piyo"' のパラメーターに割り当てることはできません。)
Object.values(SELECT_OPTIONS)
  .map<string>((value) => value)

// 配列内に value が含まれているかチェックし boolean を返す
  .includes(value)

// true の場合 value は TSelectField型 であるとコンパイラに伝える
const isTSelectField = (value: string): value is TSelectField =>

こんな感じ。

as で無理やり割り当てるのではなく、is演算子(User-Defined Type Guards)でコンパイラに対して教えてあげるのが正しいやり方となります。

以上!解決!!!

まとめ

今回はReactでselectフォームを取り扱う際のType Guardについて解説しました。

基本的にはasで無理やり型を割り当てるのではなく、is演算子(User-Defined Type Guards)で型安全なフォームの値を取り扱うのが良いと思います。

Reactを使っていると割と使う頻度が高いと思うので、この際に是非マスターしてみてください。

今回使った isTSelectField はジェネリクスで汎用的なType Guard関数にしても良いかもしれませんね。

(気が向いたら今度記事にします。)

以上。最後までご覧くださりありがとうございました!

React

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次