const schema = z.object({
// First level - primitives
name: z.string(),
email: z.string().email(),
age: z.number(),
isActive: z.boolean(),
// Nested object
profile: z.object({
bio: z.string(),
website: z.string().url().optional(),
followers: z.number(),
verified: z.boolean(),
}),
// Array of objects
addresses: z.array(z.object({
street: z.string(),
city: z.string(),
zipCode: z.string(),
isPrimary: z.boolean(),
})),
// Deeply nested
settings: z.object({
notifications: z.object({
email: z.boolean(),
sms: z.boolean(),
frequency: z.string(),
}),
preferences: z.object({
theme: z.string(),
language: z.string(),
}),
}),
});These are actual components using ValidPaths. Hover over the name prop in your IDE to see IntelliSense!
nameemailprofile.biosettings.preferences.themeageprofile.followersisActiveprofile.verifiedsettings.notifications.emailprofileaddresses.0settings.notificationsTip: Try changing the name prop values above to invalid paths - TypeScript will show an error!
All paths where the value type is string
type StringPaths = ValidPaths<typeof schema, never, never, string>nameemailprofile.bioprofile.websiteaddresses.0.streetaddresses.0.cityaddresses.0.zipCodesettings.notifications.frequencysettings.preferences.themesettings.preferences.languageAll paths where the value type is number
type NumberPaths = ValidPaths<typeof schema, never, never, number>ageprofile.followersAll paths where the value type is boolean
type BooleanPaths = ValidPaths<typeof schema, never, never, boolean>isActiveprofile.verifiedaddresses.0.isPrimarysettings.notifications.emailsettings.notifications.smsFilter paths by object shape - useful for nested form sections
type Profile = z.infer<typeof schema>['profile']type ProfilePaths = ValidPaths<typeof schema, never, never, Profile>profiletype Address = z.infer<typeof schema>['addresses'][number]type AddressPaths = ValidPaths<typeof schema, never, never, Address>addresses.0type Notifications = z.infer<typeof schema>['settings']['notifications']type NotificationsPaths = ValidPaths<typeof schema, never, never, Notifications>settings.notifications1. Type-safe form field components: Create input components that only accept paths matching their expected value type.
// Only accepts paths that resolve to string values
// TPath is auto-inferred from the 'name' prop!
function StringInput<
TSchema extends z.ZodType,
TPath extends ValidPaths<TSchema, never, never, string>,
>({ schema, name }: { schema: TSchema; name: TPath }) {
void schema; // used for type inference
const { register } = useFormContext();
return <input {...register(name)} />;
}
// Usage - no type params needed:
<StringInput schema={schema} name="profile.bio" /> // ✅ OK
<StringInput schema={schema} name="age" /> // ❌ Error: number2. Number-only inputs with validation:
function NumberInput<
TSchema extends z.ZodType,
TPath extends ValidPaths<TSchema, never, never, number>,
>({ schema, name, min, max }: {
schema: TSchema;
name: TPath;
min?: number;
max?: number;
}) {
void schema;
const { register } = useFormContext();
return <input type="number" min={min} max={max} {...register(name)} />;
}
// Usage:
<NumberInput schema={schema} name="age" min={0} max={120} /> // ✅ OK
<NumberInput schema={schema} name="profile.followers" /> // ✅ OK
<NumberInput schema={schema} name="name" /> // ❌ Error3. Object section components with useWatch:
type Profile = z.infer<typeof schema>['profile'];
// TPath is auto-inferred from the 'name' prop - no manual type params!
function ProfileSection<
TSchema extends z.ZodType,
TPath extends ValidPaths<TSchema, never, never, Profile>,
>({ schema, name }: { schema: TSchema; name: TPath }) {
const { control } = useFormContext();
const profileData = useWatch({ control, name });
// profileData is typed as Profile: { bio, website, followers, verified }
return (
<div>
<p>Bio: {profileData.bio}</p>
<p>Followers: {profileData.followers}</p>
</div>
);
}
// Usage - TPath inferred automatically:
<ProfileSection schema={schema} name="profile" /> // ✅ OK
<ProfileSection schema={schema} name="settings" /> // ❌ Error: wrong type4. Array item rendering with type safety:
type Address = z.infer<typeof schema>['addresses'][number];
function AddressCard<TPath extends ValidPaths<typeof schema, never, never, Address>>({
name,
}: {
name: TPath;
}) {
const { control } = useFormContext();
const address = useWatch({ control, name });
// address is typed as Address: { street, city, zipCode, isPrimary }
return (
<div className={address.isPrimary ? 'border-primary' : ''}>
<p>{address.street}, {address.city} {address.zipCode}</p>
</div>
);
}
// Usage with useFieldArray:
const { fields } = useFieldArray({ control, name: 'addresses' });
{fields.map((_, index) => (
<AddressCard key={index} name={`addresses.${index}`} /> // ✅ Type-safe
))}5. Bulk operations on paths of same type:
// Reset all boolean fields to false
type BoolPaths = ValidPaths<typeof schema, never, never, boolean>;
const booleanFields: BoolPaths[] = [
'isActive',
'profile.verified',
'settings.notifications.email',
'settings.notifications.sms',
];
function resetBooleans(form: UseFormReturn<z.infer<typeof schema>>) {
booleanFields.forEach((path) => {
form.setValue(path, false); // ✅ Type-safe: path resolves to boolean
});
}