はじめに
こんにちは、株式会社TOKOSのツキヤです!
今回は、React(Next.js)でフォームを作る際にほぼ必須のreact-hook-formと、そのバリデーションを良い感じにカスタムできるzodのややニッチ?な説明をします!
具体的には、ラジオボタンの値をboolean
として上手く扱う方法です!
今までラジオボタンを使う時に上手く値や型を処理できていなかった人は最後まで見て下さい💪
対象読者・前提
- TypeScript + react-hook-form + zod を使ったことがある人
- ラジオボタンの実装方法に困っていた方
今回は「TypeScript + react-hook-form + zod 」の大枠の実装方法の説明は省略しています🙏
また、本記事で述べているラジオボタンは<input type="radio" />
のようなコードで表現するものとなっています。
ラジオボタンの問題点
ラジオボタンを実装する時に問題となるのが、選択時のvalue
をstring
でしか扱えないことです🥲
<input type="radio" name="isAdmin" value="true" />
<label>管理者にする</label>
<input type="radio" name="isAdmin" value="false" />
<label>管理者にしない</label>
上記のようにboolean
な値を持つラジオボタンを作ろうとしても、value="true"
やvalue="false"
で取得できるのは文字列の"true"
, "false"
になってしまうので本当に欲しいboolean
を取得することができないです🫠
「TypeScript + react-hook-form + zod 」で問題の再現
上記の問題を「TypeScript + react-hook-form + zod 」で表現すると、
// スキーマの定義
const schema = z.object({
// ここではbooleanとして定義している
isAdmin: z.boolean({
invalid_type_error: "booleanで入力してください!!",
}),
});
// スキーマから型の生成
type Schema = z.infer<typeof schema>;
// コンポーネント部分
export function ZodForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<Schema>({
resolver: zodResolver(schema),
});
// 送信ボタン押下後の処理
const onSubmit = (params: Schema) => {
console.log(params);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<div className="flex gap-x-2">
{/* ラジオボタン */}
<input
{...register("isAdmin")}
type="radio"
id="true"
value="true"
name="isAdmin"
/>
<label htmlFor="true">管理者にする</label>
</div>
<div className="flex gap-x-2">
<input
{...register("isAdmin")}
type="radio"
id="false"
value="false"
name="isAdmin"
/>
<label htmlFor="false">管理者にしない</label>
</div>
{errors.isAdmin && (
<p>{errors.isAdmin.message}</p>
)}
</div>
<button type="submit">送信</button>
</form>
);
}
上記コードの4行目でisAdmin: z.boolean
としていますが、先程述べた通りラジオボタンはstring
しか扱えないのでバリデーションエラーに引っかかってしまいます。
ラジオボタンで上手くbooleanを扱う
スキーマ変更による修正
なので、仕方なしに上記コードのzodのスキーマは以下のように修正します
// スキーマの定義
const schema = z.object({
// enumに変更
isAdmin: z.enum(["true", "false"]),
});
// スキーマから型の生成
type Schema = z.infer<typeof schema>;
// 以下略
z.enum(["true", "false"])
は「文字列の"true"
か"false"
のみ許容」というバリデーションです。
この記述にすることで、「チェックをしてもバリデーションエラーになる」問題は解消しました!
しかし、本来扱いたいのは真のboolean
なので、「文字列の"true"
か"false"
」を上手くboolean
に変更したいです🤔
transformによる修正
ここで、zodのtransform
という関数を使います!transform
を使うことで、バリデーション後に任意の値に変換することができます✨transform
バージョンを記述します。
// スキーマの定義
const schema = z.object({
// ここではbooleanとして定義している
isAdmin: z.enum(["true", "false"]).transform((val: "true" | "false") => val === "true"),
});
// スキーマから型の生成
type Schema = z.infer<typeof schema>;
// 以下略
transform((val: "true" | "false") => val === "true")
で、元々の値val
が"true"
かどうかの比較した結果をreturn
しています。
こうすることで、実際のboolaen
に変換することができました✨
しかし、機能的には問題無いですが型定義部分がやや微妙になっています。type Schema = z.infer<typeof schema>;
で生成される型は{ isAdmin: boolean }
となります。
一見問題無さそうなのですが、例えばdefaultValuesで初期値を設定しようとするとエラーになってしまいます。
この型エラーに対する問題を解消していきます!
型定義にinput, outputを使用
具体的には、まずzodのスキーマから型を生成する時に、infer
の代わりにinput
とoutput
というメソッドを使います。
// スキーマの定義
const schema = z.object({
isAdmin: z.enum(["true", "false"]).transform((val: "true" | "false") => val === "true"),
});
// スキーマから型の生成
type InputSchema = z.input<typeof schema>;
type OutputSchema = z.output<typeof schema>;
// 以下略
input
はtransform
前の値の型を、output
はtransform
後の値の型を生成できます!
中身を見ると、InputSchema
→ { isAdmin: "true" | "false" }
、OutputSchema
→ { isAdmin: boolean }
となっています。
そして、できた型をuseForm部分に適用し直します!
export function ZodForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<InputSchema, unknown, OutputSchema>({
resolver: zodResolver(schema),
defaultValues: {
isAdmin: "false",
},
});
// 以下略
useForm<InputSchema, unknown, OutputSchema>
とすることで、フォームの中の値を扱う時はInputSchemaを、バリデーション後にはOutputSchema
を使うことができます✨
よって、先程出ていたdefaultValues
部分のエラーも出なくなり、onSubmit
の引数もbooelan
としてisAdmin
の値が取得できるようになりました💪
おわりに
今回は「TypeScript + react-hook-form + zod 」でよく有りそうかつ問題になりそうなラジオボタンを題材にしてみました!
需要があれば他のinputやもっと複雑なUI・仕様のフォームの解説もしようと思います😎