gaaamiiのブログ

間違ったことを書いている時があります。コメントやTwitter、ブコメなどでご指摘ください

TypeScriptのkeyofの使いどころ

読者登録したブログ記事をだらだらと巡回していて、こちらの記事を拝見しました。淡々と学んだことをブログに書いていてすごいなと思っていつも読ませてもらっています。

yurufuwa-tech.hatenablog.com

特定のオブジェクトのkeyの値しか許容しない、みたいなパターンの時に使えるって話なんだとは思うのだがこれがどこで使えるのか…というのはちょい謎。。。

TypeScriptは覚えることが多くて自分もあまり自信がないのですが、keyofについてはちょうど最近便利だな〜と感じたことがあったので、傲慢にも勝手にアンサー記事みたいなのを書こうと思いました(間違ってること言ってたらご指摘ください)。釈迦に説法だったら生温かい目で見てください。

Formikのフィールド名に使える

具体的すぎ感ありますが、keyofはFormikのフィールド名を表現するのに使えました。

FormikというのはReactでフォームを扱う際に便利コンポーネントを提供してくれるライブラリです。Formikを利用したフォームは、通常のHTMLフォームと同様、フォームがあって、そのなかにフィールドがあって、それぞれのフィールドにはname属性がついている、みたいな形になります。

const MyInnerForm = (props: Props) => {
  return (
    <Form>
      <Field name="hoge" />
    </Form>
  )
}

Formikを使うと、↑こんな感じでフォーム要素を記述して、 ↓withFormikというHOCで必要なコールバックなどを流し込んだコンポーネントを作れます。

const handleSubmit = (values: Values) => { /* ...省略 */ }
const validate = (values: Values, props: Props)  => { /*...省略 */ }
const MyForm = withFormik({ hanldeSubmit, validate, /* ほかにもいろいろ入れるけど省略 */,  })(MyInnerForm)

Formikは、ユーザーがそのフィールドを触ったかどうかとか、validateで判定したエラーの結果なんかをFormikのpropsとして持っています。

なので、エラーがあるときはこんな形で参照できます。

interface Values {
  hoge: string;
  fuga: string;
  foo: string;
}

const MyInnerForm = (props: Props & FormikProps<Values>) => {
  console.info(props.touched.hoge)
  console.info(props.errors.hoge)

  return (
    <Form>
      <Field name="hoge" />
    </Form>
  )
}

で、場合によってはここで以下のような hasError みたいな関数を定義したくなります(なりました)。 使いやすい、イケてるウェッブサービスをつくりたいので、hasErrorだったときはその場でフィールドを赤くしたりしたくなるのです。

const hasError = (props: Props & FormikProps<Values>): boolean => {
  return props.touched.hoge && props.errors.hoge
}

いいんじゃないでしょうか。しかしこれが複数のフィールドになると面倒です。

const hasErrorOnHoge = (props: Props & FormikProps<Values>): boolean => {
  return props.touched.hoge && props.errors.hoge
}
const hasErrorOnFuga = (props: Props & FormikProps<Values>): boolean => {
  return props.touched.fuga && props.errors.fuga
}
const hasErrorOnFoo = (props: Props & FormikProps<Values>): boolean => {
  return props.touched.foo && props.errors.foo
}

関数1つにしたいですね。フィールド名を渡すようにします。

const hasError = (props: Props & FormikProps<Values>, fieldName: string): boolean => {
  return props.touched[fieldName] && props.errors[fieldName]
}

できた〜。イケてる〜。

となるんですが、問題は、fieldNameの許容する値が広いことです。

hasError(props, 'hogeee')

こうしても、型のエラーにはなりません。stringなのだからそりゃそうです。

ここでようやく、keyof の出番です。これでどうでしょう。

const hasError = (props: Props & FormikProps<Values>, fieldName: keyof Values): boolean => {
  return props.touched[fieldName] && props.errors[fieldName]
}
hasError(props, 'hoge')
hasError(props, 'hogeee') // Argument of type '"hogeee"' is not assignable to parameter of type "hoge" | "fuga" | "foo"

打ち間違えてhogeeeにしたらちゃんと怒られるようになりました。めでたし。

まとめ

TypeScriptほんと難しくてあれやこれや覚えないとな〜という感じで、自分で型書いているときはあまり難しいことはできていないのですが、keyofはわりと出番がありそうだなと思ってます。

関連