- Published on
React Hook Form, Zod and Nested Objects: Why Nested Paths Aren't Recognized
TL;DR
When composing Zod schemas with intermediate objects defined as separate constants and using React Hook Form, TypeScript may throw errors on nested paths in the name
field of <FormField />
components. The solution: declare nested objects directly within the parent schema.
The Problem
Consider a real-world example: creating a personal information form with a nested structure.
The Problematic Approach
Here’s how one might initially structure the schema using intermediate objects:
const zIdentity = z.object({
gender: z.enum(["male", "female", "other"]),
first_name: z.string(),
last_name: z.string(),
});
const schema = z.object({
personal_information: z.object({
identity: zIdentity,
// ... other fields
}),
});
In the React component:
<FormField
control={form.control}
name="personal_information.identity.gender"
render={({ field }) => (
<Select value={field.value} onValueChange={field.onChange} />
)}
/>
The TypeScript Error
This approach results in the following error:
Type '"personal_information.identity.gender"' is not assignable to type ...
Did you mean '"personal_information.identity"'?
Understanding the Root Cause
When using intermediate schema constants like zIdentity
, TypeScript doesn’t “unfold” the nested paths for React Hook Form’s field name typing. From TypeScript’s perspective, identity
becomes an opaque object, preventing it from recognizing the nested fields like gender
or first_name
within the name
field typing.
This limitation means React Hook Form only recognizes "personal_information.identity"
as a valid field name, not the deeply nested "personal_information.identity.gender"
.
Why Other Fields Work Differently
Fields defined directly within the parent schema without intermediate constants allow TypeScript to maintain full visibility of the structure. This enables React Hook Form to generate proper nested path typing for the name
field.
The Solution
The fix is straightforward: define nested objects directly within the parent schema instead of using intermediate constants. It’s mean you need to copy paste (so saaaad :( )
Before - Problematic Structure:
gender: z.enum(["male", "female", "other"]),
first_name: z.string(),
last_name: z.string(),
});
const schema = z.object({
personal_information: z.object({
identity: zIdentity,
// ...
}),
});
After - Working Structure:
personal_information: z.object({
identity: z.object({
gender: z.enum(["male", "female", "other"]),
first_name: z.string(),
last_name: z.string(),
}),
// ...
}),
});
With this structure, React Hook Form correctly recognizes nested paths:
control={form.control}
name="personal_information.identity.gender"
render={({ field }) => (
<Select value={field.value} onValueChange={field.onChange} />
)}
/>
TypeScript validation now works seamlessly.
Key Takeaways
- Cause: Intermediate Zod schema constants aren’t “unfolded” by TypeScript for React Hook Form’s typing system.
- Symptom: React Hook Form doesn’t recognize nested paths in the
name
field. - Solution: Define nested objects directly within the parent schema.
Best Practices
This issue stems from how TypeScript infers types for nested paths rather than limitations in Zod or React Hook Form themselves. For complex forms requiring strict TypeScript validation, always declare nested objects inline within the parent schema.
This approach ensures full compatibility between Zod validation, React Hook Form’s field management, and TypeScript’s type checking system.