背景
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>
T に Inputs を当てはめると次のようになる
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> を使うことで以下を一度に定義できる
-
dataはInputs型 -
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 の関数型定義。InputsをTに流し込むことで「data の型 + event 省略可 + 戻り値 void/Promise」を一発で定義できる
感想
- 最初は
SubmitHandler<Inputs>を「なんかライブラリのおまじない」くらいに思ってたが、<T>を当てはめた後の姿を書き下してみると一気にスッキリした