Replies: 2 comments
-
|
zod v4 changed how Schema type works. the fix is to use ZodTypeAny instead of Schema: import { zodResolver } from "@hookform/resolvers/zod";
import { useForm, UseFormProps, FieldValues } from "react-hook-form";
import { z, ZodTypeAny } from "zod";
export const useZodForm = <
TSchema extends ZodTypeAny,
TFieldValues extends FieldValues = z.infer<TSchema>,
>(
props: {
schema: TSchema;
} & Omit<UseFormProps<z.infer<TSchema>>, "resolver">,
) => {
const { schema, ...rest } = props;
return useForm<TFieldValues>({
resolver: zodResolver(schema),
...rest,
});
};ZodTypeAny is alternatively you can add type assertion: export const useZodForm = <TSchema extends z.ZodType>(
schema: TSchema,
options?: Omit<UseFormProps<z.infer<TSchema>>, "resolver">,
) => {
return useForm({
resolver: zodResolver(schema),
...options,
} as UseFormProps<z.infer<TSchema>>);
};the ZodTypeAny approach is cleaner and works without casting |
Beta Was this translation helpful? Give feedback.
-
why this breaksthe root cause is a change in how zod's base type generics default. zod v3: // ZodType defaults all generics to `any`
abstract class ZodType<Output = any, Def extends ZodTypeDef = ZodTypeDef, Input = Output> { ... }
// z.Schema was just an alias
export { ZodType as Schema, ZodType as ZodSchema };
// ZodTypeAny = ZodType<any, any, any>
export type ZodTypeAny = ZodType<any, any, any>;
// inference used direct property access
export type output<T extends ZodType<any, any, any>> = T["_output"];zod v4: // ZodType defaults all generics to `unknown`
interface ZodType<out Output = unknown, out Input = unknown, out Internals extends ...> { ... }
// Schema, ZodSchema, ZodTypeAny are all deprecated aliases for ZodType
export type {
ZodType as ZodTypeAny, // deprecated, just `ZodType` now
ZodType as ZodSchema, // deprecated
ZodType as Schema, // deprecated
};
// inference uses conditional types against `_zod` internals
export type output<T> = T extends { _zod: { output: any } } ? T["_zod"]["output"] : unknown;so when you write the important thing to understand is that why
|
| constraint | inference | structural match | recommendation |
|---|---|---|---|
z.Schema / z.ZodType (default) |
unknown |
yes | won't work with FieldValues |
z.Schema<any, any> |
any |
often fails on complex schemas | fragile |
z.core.$ZodType<any, any> |
any (then narrows on usage) |
yes (minimal interface) | recommended |
{ _zod: { output: any; input: any } } |
works via conditional | yes | works but needs as any on zodResolver |
relevant links:
- zod v4 library authors guide - explains
z.core.$ZodTypefor library authors - zodResolver source - how
@hookform/resolvershandles both v3 and v4 - zod v4 core types -
output<T>conditional type definition
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Background
In an application I work on, we have a utility hook
useZodFormthat wrapsuseFormwithzodResolverto simplify form setup with zod validation and get the correct type inference for the form methods based on the zod schema provided.This worked perfectly in zod v3:
In zod v3,
z.infer<TSchema>whenTSchema extends z.Schemawould infer toany, which was compatible withFieldValueswhich evaluates toRecord<string, any>.The Problem
After upgrading to zod v4,
z.Schemais now defined asZodType<unknown, unknown, ...>, which means when you constrain a generic type parameter asTSchema extends z.ZodType, TypeScript can only inferunknownforz.infer<TSchema>,z.input<TSchema>, andz.output<TSchema>.for example:
I tried using
z.Schema<any, any>as a constraint, to bring us back closer to the old, zod v3 behavior. This works great for type inference:The problem is that when we try to use it in practice, Typescript tries to evaluate whether the schema we pass in structurally matches
ZodType<any, any>, which it does not and things fail:Question
I'm running a bit out of ideas on how to deal with this in a way that does not involve type casting or
@ts-expect-error😅Anyone has figured out the right way to deal with this yet?
Versions used for info:
Beta Was this translation helpful? Give feedback.
All reactions