はじめに
こんにちは、株式会社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・仕様のフォームの解説もしようと思います😎
ISSEN 
