TypeScript + Zod + React Hook Form の組み合わせで開発していると、時々予期しない型エラーに遭遇することがあります。今回は、Zod の coerce 機能を使用した際に発生した型の不一致エラーとその解決方法について紹介します。
今回遭遇したエラーについて
React Hook Form でフォームバリデーションを実装している際、以下のようなエラーが発生しました。
import { z } from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
// Zodスキーマ定義
const schema = z.object({
age: z.coerce.number(),
name: z.string(),
});
type FormData = z.infer<typeof schema>;
const MyForm = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(schema), // error: Type 'unknown' is not assignable to type 'number'
});
};
発生したエラーメッセージ
Type 'unknown' is not assignable to type 'number'
解決方法
エラーの解決は非常にシンプルで、Zod のスキーマ定義を以下のように修正するだけです
// 修正後のコード
const schema = z.object({
age: z.coerce.number<number>(), // <number> を明示的に指定
name: z.string(),
});
この修正により、型エラーが解消され、React Hook Form が期待する型定義と一致するようになります。
動作確認済み環境
この問題と解決方法は以下のバージョンで確認しています
- Node.js: 20.18.0
- Zod: 4.1.11
- React Hook Form: 7.63.0
Zod とは
Zod は TypeScript ファーストなスキーマバリデーションライブラリです。
- 型安全なバリデーション
- 豊富なバリデーションルール
- TypeScript の型推論との優れた統合
- ランタイムでの型チェック機能
フォームデータやAPI レスポンスの検証によく使用され、開発時とランタイム両方での型安全性を提供します。
coerce
coerce は、受け取った値を指定した型に変換する機能です。例えば z.coerce.number() は文字列 "123" を数値 123 に変換します。フォームから送信される文字列データを適切な型に変換する事が可能になります。
React Hook Form とは
React Hook Form は、React でフォームを効率的に管理するためのライブラリです。
- 最小限の再レンダリング
- 簡単なフォームバリデーション
- TypeScript との優れた統合
- パフォーマンスに優れた設計
Zod と組み合わせることで、強力で型安全なフォーム管理が実現できます。
エラーの原因と解決方法の詳細
エラーが発生する状況
問題は z.coerce.number() の型推論にありました:
// 問題のあるスキーマ定義
const problematicSchema = z.object({
age: z.coerce.number(), // 型が unknown として推論される
});
type ProblematicFormData = z.infer<typeof problematicSchema>;
// ProblematicFormData = { age: unknown }
この定義では、TypeScript が age フィールドの型を unknown として推論してしまいます。React Hook Form は厳密な型チェックを行うため、number を期待しているフィールドに unknown が割り当てられることでエラーが発生していました。
解決方法の詳細
zodのissueの投稿内容を見つけて解決することができました。
解決方法は、coerce.number() に明示的な型パラメータを指定することです
// 正しいスキーマ定義
const correctSchema = z.object({
age: z.coerce.number<number>(), // 明示的に number 型を指定
});
type CorrectFormData = z.infer<typeof correctSchema>;
// CorrectFormData = { age: number }
修正後の全体コード
修正後の全体のコードは以下のとおりです
import React from 'react';
import { z } from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
// 修正されたスキーマ定義
const formSchema = z.object({
age: z.coerce.number<number>().min(0, '年齢は0以上で入力してください'),
name: z.string().min(1, '名前を入力してください'),
});
type FormData = z.infer<typeof formSchema>;
const UserForm: React.FC = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(formSchema),
});
const onSubmit = (data: FormData) => {
console.log(data);
// data.age は number 型として正しく推論される
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="name">名前:</label>
<input
{...register('name')}
type="text"
id="name"
/>
{errors.name && <p>{errors.name.message}</p>}
</div>
<div>
<label htmlFor="age">年齢:</label>
<input
{...register('age')}
type="number"
id="age"
/>
{errors.age && <p>{errors.age.message}</p>}
</div>
<button type="submit">送信</button>
</form>
);
};
export default UserForm;
まとめ
Zod の coerce 機能と React Hook Form を組み合わせて使用する際は、明示的な型パラメータの指定が必要です:
-
問題:
z.coerce.number()がunknown型として推論される -
解決:
z.coerce.number<number>()として明示的に型を指定 - 結果: TypeScript と React Hook Form の型システムが正しく連携
この修正により、型安全性を保ちながら文字列から数値への変換機能を活用できます。同様の型エラーに遭遇した際は、coerce メソッドに適切な型パラメータを指定することで解決できます。