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.

Last updated
© 2025, Devpulsion.
Exposio 1.0.0#82a06ca | About