🤔 こんな問題に悩んでいませんか?
React・React Hook Form・Zodを組み合わせてフォームを作っていると、
<input type="number">
の値が「数値」ではなく「文字列」として扱われてしまうことがあります。
たとえば
const value = formData.get('count'); // フォーム送信後の値
console.log(typeof value); // "string" 😱
見た目は数値用のフォーム要素なのに、
React側・サーバー側では結局 string として値を受け取ってしまい、
「数値として処理したいのに!」と困ることはありませんか?
📝 よくある解決パターン
こんな解決パターンを見かけることがあります。
// フォーム用とサーバー用、2つの型定義を作成
interface FormData {
count: string; // フォームからの入力は文字列
}
interface ProcessedData {
count: number; // 処理時は数値
}
このパターンだと、コードが冗長になり、型の不一致によるバグも発生しやすい問題があります。
✅ Zodのcoerce機能で解決!
Zodのcoerce.number()
を使えば、この問題を解決できます。
import { z } from "zod";
// たった1つの型定義でOK! 🎉
const formSchema = z.object({
count: z.coerce.number().int().min(1)
});
type FormData = z.infer<typeof formSchema>;
🚀 React Hook Formでの実装例
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
function CounterForm() {
const { register, handleSubmit } = useForm<FormData>({
resolver: zodResolver(formSchema)
});
const onSubmit = (data: FormData) => {
console.log(typeof data.count); // "number" 🎯
// 数値として直接計算できる!
const doubled = data.count * 2;
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="number" {...register("count")} />
<button type="submit">送信</button>
</form>
);
}
⚡ Next.jsのServer Actionsでも
'use server'
// Server Actionsでも同じスキーマが使える! 🔄
export async function saveCount(formData: FormData) {
const data = formSchema.parse({
count: formData.get('count')
});
// data.countは既に数値型 🧮
// データベースに保存する処理...
}
💡 coerce.number()のポイント
- 空文字列 →
0
に変換 - 文字列
"123"
→ 数値123
に変換 - バリデーションも同時に設定可能(
.int()
、.min()
など)
🌟 メリット
- 型定義の一元化: フォーム用とサーバー用で別々の型が不要に
- コードの簡素化: 変換処理を書く手間が省ける
- 型安全性の向上: フォーム入力からサーバー処理まで一貫した型で扱える
🎯 まとめ
React・React Hook Form・Zodを組み合わせてフォームを使うと、
の値が文字列として扱われるケースがあります。
Zodの coerce.number() を使うことで、
型の管理をシンプルにしつつ、自然に数値として扱いやすくできました!