👹

【React Hook Form】バリデーションスキーマのあいまいな型注釈をTypeScript は受け付けない

2025/04/07に公開

はじめに

先日、React Hook Formとバリデーションライブラリにyupを使っているReactプロジェクトのアップデートを行いました。

このアップデート後に、React Hook Formresolver部分で型エラーが発生しました。
ちなみに、当エラーはフォーム送信の挙動に何ら影響を与えるのものではなかったのですが当然放置するわけにもいかないので対応しました。

const {register,handleSubmit,reset,formState: { errors },setValue} = useForm<RegFormSchema>({
        defaultValues:
            mail: '',
            name: '',
            // 中略
        },
        resolver: yupResolver(regFormSchema), // ここでエラー発生
    });
  • 型エラーの内容(一部)
型 'Resolver<{ mail: string | undefined; name: string; companyName: string | undefined; addressNumber: string | undefined; address: string; tel: string | undefined; content: string; }, any, { ...; }>' を型 'Resolver<{ mail?: string | undefined; companyName?: string | undefined; addressNumber?: string | undefined; tel?: string | undefined; name: string; address: string; content: string; }, any, { ...; }>' に割り当てることはできま...

今回の一件で筆者は、「yup(使用するバリデーションライブラリ)のあいまいな型注釈をTypeScriptは受け付けないのでTypeScriptが期待した型注釈にスキーマの内容を修正する必要がある」ということを知れたので、備忘録も兼ねて記事にしていきます。

  • 発生時の環境
    アップデート前後の差分は以下です。
- @hookform/resolvers@3.3.4
- eslint-plugin-react-hooks@5.1.0
- react-hook-form@7.51.2
- yup@1.4.0
+ @hookform/resolvers@5.0.1
+ eslint-plugin-react-hooks@5.2.0
+ react-hook-form@7.55.0
+ resolvers@1.0.0
+ yup@1.6.1

つまり、今回起きた出来事は上記の設定やバージョンにおいて、となります。

結論

先の型エラーの内容(一部)にありますが、以下のようなオプショナルに関する差分が原因でした。

// 以下の型は
mail: string | undefined;
companyName: string | undefined;
addressNumber: string | undefined;
...
..
.
// 以下の型に割り当てられない
mail?: string | undefined;
companyName?: string | undefined;
addressNumber?: string | undefined;

対応(yupスキーマの内容を修正)

export const regFormSchema = yup.object({
-    mail: yup.string().matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, '適切な形式でメールアドレスを入力してください'),
+    mail: yup.string().required('メールアドレスを入力してください')
+        .matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, '適切な形式でメールアドレスを入力してください'),
    name: yup.string().required('お名前を入力してください'),
-    companyName: yup.string(),
-    addressNumber: yup.string().max(7).matches(/\d{7}/, '7 桁の数字で入力してください'),
+    companyName: yup.string().required('会社名を入力してください'),
+    addressNumber: yup.string().required('郵便番号を入力してください')
+        .max(7).matches(/\d{7}/, '7 桁の数字で入力してください'),
    address: yup.string().required('住所は必須項目です'),
-    tel: yup.string().matches(japanesePhoneRegex, '{080-xxxx-xxxx, 075-xxxx-xxxx, 03-xxxx-xxxx}など適切な形式で電話番号を入力してください'), // OR(|)が正しく機能するようにするには不要なスペースを入れない
+    tel: yup.string().required('電話番号を入力してください')
+        .matches(japanesePhoneRegex, '{080-xxxx-xxxx, 075-xxxx-xxxx, 03-xxxx-xxxx}など適切な形式で電話番号を入力してください'),
    content: yup.string().required('問い合わせ内容は必須項目です'),
});

export type RegFormSchema = InferType<typeof regFormSchema>;

少しごちゃごちゃしていますが行ったのは「必要項目に.requiredを付けた(型を明示的にした)」くらいです。

この修正によって以下のような型になり、型エラーも解消されました。

Resolver<{
    mail: string;
    name: string;
    companyName: string;
    addressNumber: string;
    address: string;
    tel: string;
    content: string;
}... // 中略

さて、先の型エラーを確認します。

// 以下の型は
mail: string | undefined;
companyName: string | undefined;
addressNumber: string | undefined;
... // 中略
..
.
// 以下の型に割り当てられない
mail?: string | undefined;
companyName?: string | undefined;
addressNumber?: string | undefined;

正直、オプショナルがあるかないかだけで型の処理内容は同じだと思うのですが、
TypeScript的には No だったようです。

.requiredを付けて明示的な型注釈にすることで解消されました。

Claude に聞いてみる

先のエラー対応について釈然としない部分があったので尋ねてみました。

問題の原因は、InferType<typeof regFormSchema> から生成された型と useForm の期待する型の間の不一致にありました。
具体的に説明すると:

  1. Yupスキーマでは required() を指定していないフィールドは、バリデーション上はオプショナルになります(入力しなくてもバリデーションエラーにならない)
  2. しかし、TypeScriptの型としては、yup.string() だけでは「string型または undefined型」とは解釈されず、「string型」として解釈されます

先に筆者は「正直、オプショナルがあるかないかだけで型の処理内容は同じだと思うのですが」と述べましたが、この違いが重大だったようです。

前者は「プロパティは必ず存在する(値はundefinedかもしれないが)」、後者(オプショナル)は「プロパティ自体が存在しないかもしれない」という違いがあり、この微妙な違いが型エラーの原因です。

まとめ

React Hook Formresolver部分で型エラーが発生した場合はスキーマ設定を疑ってみる。


今回の一件で「yup(使用するバリデーションライブラリ)のあいまいな型注釈をTypeScriptは受け付けないのでTypeScriptが期待した型注釈にスキーマの内容を修正する必要がある」ということを知りました。

根本原因は、TypeScript(のオプショナルプロパティ)に対する筆者の理解不足だったと思います。
今後も、無知の知をモットーにキャッチアップとアウトプットで身につけていきたいと思います。

ここまで読んでいただき、ありがとうございました。

Discussion