🍊

React Hook FormとZodを使ったデータ変換と型整合性の維持

2024/05/20に公開

結論

  • React Hook Formのv7.44.0からuseFormが変換前と変換後のジェネリクス型に対応しているのでそれを使うことで、zodでデータ変換をして、型整合性も維持できる
  • useForm<TFieldValues, TContext, TTransformedValues>
  • useForm<入力の型、コンテキストの型、出力の型>

背景

  • React Hook FormとZodを使ってフォームを作成しました
  • 送信するデータは、name, email, contentの3つのフィールドを持つオブジェクトです
  • フォームではnameを姓と名に分けて入力させ、送信時にそれをnameフィールドに結合した形で送りたいと考えました
  • onSubmit関数内でデータ変換を行う方法もありますが、フォームロジックとデータ変換ロジックを分離するために、Zodのtransform機能を使いたいと考えました
  • 参考記事:react-hook-form と zod でバリデーションのその先へ

問題

  • useFormに入力の型を指定し、onSubmitに出力の型を指定すると、型不整合が発生します
  • これは、zodResolverなどを使ってvalidationする際に、zod.transform()を使うと、validationの前後でデータを変換するためです
  • 具体的には、onSubmit関数の定義で型 { name: string; email: string; content: string; } を期待するが、useFormでは { name: { first_name: string; last_name: string; }; email: string; content: string; } という型を使用しています
  • この不整合により、以下のようなエラーが発生します:
'SubmitHandler<{ name: string; email: string; content: string; }>' の引数を型 'SubmitHandler<{ name: { first_name: string; last_name: string; }; email: string; content: string; }>' のパラメーターに割り当てることはできません。
  型 '{ name: { first_name: string; last_name: string; }; email: string; content: string; }' を型 '{ name: string; email: string; content: string; }' に割り当てることはできません。
    プロパティ 'name' の型に互換性がありません。
      型 '{ first_name: string; last_name: string; }' を型 'string' に割り当てることはできません。

参考実装(省略版)

// 〜〜〜〜省略〜〜〜〜〜
// お問い合わせフォームのスキーマ
const formSchema = z.object({
  // 入力の型(変換前)
  name: z.object({
    first_name: z.string().min(1, { message: "入力が必須の項目です" }),
    last_name: z.string().min(1, { message: "入力が必須の項目です" }),
  }),
  email: z.string().email({ message: "メールアドレスの形式が正しくありません" }),
  content: z.string(),
}).transform(({ name, email, content }) => ({
  // 出力の型(変換後)
  name: `${name.last_name} ${name.first_name}`,
  email,
  content,
}));

// zod は、input と output で変換前と変換後の型を取得できる
type InputType = z.input<typeof formSchema>;
type OutputType = z.output<typeof formSchema>;

// お問い合わせフォーム
const Contact = () => {
  const form = useForm<InputType>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      name: { first_name: "", last_name: "" },
      email: "",
      content: "",
    },
  });

  const onSubmit: SubmitHandler<OutputType> = async (data) => {
    // 〜〜〜〜省略〜〜〜〜〜
  };

  return (
    // 〜〜〜〜省略〜〜〜〜〜
    <form onSubmit={form.handleSubmit(onSubmit)}></form>
    // 〜〜〜〜省略〜〜〜〜〜
  );
};

export default Contact;

解決方法

  • React Hook Formのv7.44.0からuseFormが変換前と変換後のジェネリクス型に対応したので、それを使う。
    • useForm<TFieldValues, TContext, TTransformedValues>
    • useForm<入力の型, コンテキストの型, 出力の型>
    • TContextはデフォルトでany

参考実装(省略版)

const form = useForm<InputType, any, OutputType>({
  resolver: zodResolver(formSchema),
  defaultValues: {
    name: {
      first_name: "",
      last_name: "",
    },
    email: "",
    content: "",
  },
});

const onSubmit: SubmitHandler<OutputType> = async (data) => {
  // 〜〜〜〜省略〜〜〜〜〜
};

return (
  // 〜〜〜〜省略〜〜〜〜〜
  <form onSubmit={form.handleSubmit(onSubmit)}></form>
  // 〜〜〜〜省略〜〜〜〜〜
);

まとめ

  • Zodはvalidationだけでなく、データ変換もでき、フォームの関心事とデータ変換の関心事を分けることができる
  • React Hook Formのv7.44.0からuseFormが変換前と変換後のジェネリクス型に対応したので、それを使うことで型不整合を解消できる

参考資料

Discussion