1
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?

TS 型定義 ジェネリクス `<T>` - react-hook-form

1
Posted at

背景

react-hook-form で出てくる const onSubmit: SubmitHandler<Inputs> = async (data) => { の型定義が何をしているのか分からなかったので、TypeScript の型まわりを整理してアウトプットしておく

React + Chakra UI でモーダル内にフォームを表示しようとしていた時に遭遇した

型とは

変数や関数に入れられるデータの種類のこと

const name: string = "田中"   // 文字列しか入れられない
const age:  number = 25       // 数値しか入れられない

型を書くことでコードを実行する前に間違いを検出できる

関数の型

関数は「何を受け取って、何を返すか」に型をつけられる

const double = (x: number): number => {
  return x * 2
}

戻り値が何もない場合は void を使う

const greet = (name: string): void => {
  console.log("こんにちは" + name)  // return しない
}

type で型に名前をつける

type キーワードで型に名前をつけられる

// 「number を受け取って void を返す関数」に名前をつける
type Greeter = (x: number) => void

const greet: Greeter = (x) => {
  console.log(x)
}

ジェネリクス <T>

決め打ちの型だと使い回せない

type NumberFunc = (data: number) => void  // number 専用
type StringFunc = (data: string) => void  // string 専用

<T> を使うと「後で型を当てはめるプレースホルダー」として汎用的な型を作れる

type AnyFunc<T> = (data: T) => void

type NumberFunc = AnyFunc<number>  // → (data: number) => void
type StringFunc = AnyFunc<string>  // → (data: string) => void

SubmitHandler<Inputs> を読む

// react-hook-form の定義
type SubmitHandler<T> = (data: T, event?: Event) => void | Promise<void>

TInputs を当てはめると次のようになる

type SubmitHandler<Inputs> = (data: Inputs, event?: Event) => void | Promise<void>
項目 内容
data の型 Inputs(studyContent と studyTime を持つ)
event ? があるので省略可能
戻り値 void または Promise<void> のどちらでもOK

今回のコード

const onSubmit: SubmitHandler<Inputs> = async (data) => { ... }

これは onSubmit 変数の型を SubmitHandler<Inputs> で定義している

SubmitHandler<Inputs> を使うことで以下を一度に定義できる

  • dataInputs
  • event は省略可能
  • 戻り値は void または Promise<void>
  • react-hook-form の submit 関数として使う意図

async がついているので戻り値は Promise<void> になる

実際の利用例

react-hook-form と Chakra UI のモーダルを組み合わせたフォームの例

import { useForm, SubmitHandler } from 'react-hook-form'
import { Dialog, Portal, Input, Button, Text, Stack } from '@chakra-ui/react'
import { supabase } from '../../supabaseClient'

type Props = {
  open: boolean
  onOpenChange: (open: boolean) => void
  onCreated: () => Promise<void>
}

type Inputs = {
  studyContent: string
  studyTime: number
}

export const RecordForm = ({ open, onOpenChange, onCreated }: Props) => {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<Inputs>()

  const onSubmit: SubmitHandler<Inputs> = async (data) => {
    const { error } = await supabase.from('study-record').insert([
      { title: data.studyContent, time: data.studyTime },
    ])
    if (error) {
      console.error(error)
      return
    }
    reset()
    onOpenChange(false)
    await onCreated()
  }

  const handleCancel = () => {
    reset()
    onOpenChange(false)
  }

  return (
    <Dialog.Root open={open} onOpenChange={(e) => onOpenChange(e.open)}>
      <Portal>
        <Dialog.Backdrop />
        <Dialog.Positioner>
          <Dialog.Content>
            <form onSubmit={handleSubmit(onSubmit)}>
              <Dialog.Header>
                <Dialog.Title>新規レコード</Dialog.Title>
              </Dialog.Header>
              <Dialog.Body>
                <Stack gap={4}>
                  <Stack gap={1}>
                    <Input
                      placeholder='勉強内容'
                      {...register('studyContent', {
                        required: '内容の入力は必須です',
                      })}
                    />
                    {errors.studyContent && (
                      <Text color='red.500' fontSize='sm'>
                        {errors.studyContent.message}
                      </Text>
                    )}
                  </Stack>
                  <Stack gap={1}>
                    <Input
                      type='number'
                      placeholder='勉強時間'
                      {...register('studyTime', {
                        required: '時間の入力は必須です',
                        min: {
                          value: 0,
                          message: '時間は0以上である必要があります',
                        },
                        valueAsNumber: true,
                      })}
                    />
                    {errors.studyTime && (
                      <Text color='red.500' fontSize='sm'>
                        {errors.studyTime.message}
                      </Text>
                    )}
                  </Stack>
                </Stack>
              </Dialog.Body>
              <Dialog.Footer>
                <Button type='button' variant='outline' onClick={handleCancel}>
                  キャンセル
                </Button>
                <Button type='submit'>登録</Button>
              </Dialog.Footer>
              <Dialog.CloseTrigger />
            </form>
          </Dialog.Content>
        </Dialog.Positioner>
      </Portal>
    </Dialog.Root>
  )
}

まとめ

  • TypeScript の型は「変数や関数に入る値の種類」を実行前に縛る仕組み
  • type で型に名前をつけられる。関数型も名前をつけられる
  • <T> はジェネリクス。後から型を当てはめるプレースホルダーで、AnyFunc<number> のように展開される
  • SubmitHandler<Inputs> は react-hook-form の関数型定義。InputsT に流し込むことで「data の型 + event 省略可 + 戻り値 void/Promise」を一発で定義できる

感想

  • 最初は SubmitHandler<Inputs> を「なんかライブラリのおまじない」くらいに思ってたが、<T> を当てはめた後の姿を書き下してみると一気にスッキリした
1
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
1
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?