diff --git a/zod/src/__tests__/__fixtures__/data.ts b/zod/src/__tests__/__fixtures__/data.ts index 25fa0dd0..f338f1d1 100644 --- a/zod/src/__tests__/__fixtures__/data.ts +++ b/zod/src/__tests__/__fixtures__/data.ts @@ -58,7 +58,7 @@ export const validData = { }, ], dateStr: '2020-01-01', -} as any as z.infer; +} satisfies z.input; export const invalidData = { password: '___', @@ -66,7 +66,7 @@ export const invalidData = { birthYear: 'birthYear', like: [{ id: 'z' }], url: 'abc', -} as any as z.infer; +} as unknown as z.input; export const fields: Record = { username: { diff --git a/zod/src/__tests__/zod.ts b/zod/src/__tests__/zod.ts index 086f2243..f4a88f01 100644 --- a/zod/src/__tests__/zod.ts +++ b/zod/src/__tests__/zod.ts @@ -1,4 +1,4 @@ -import { FieldValues, Resolver, SubmitHandler, useForm } from 'react-hook-form'; +import { Resolver, SubmitHandler, useForm } from 'react-hook-form'; import { z } from 'zod'; import { zodResolver } from '..'; import { fields, invalidData, schema, validData } from './__fixtures__/data'; @@ -99,7 +99,7 @@ describe('zodResolver', () => { const resolver = zodResolver(z.object({ id: z.number() })); expectTypeOf(resolver).toEqualTypeOf< - Resolver + Resolver<{ id: number }, unknown, { id: number }> >(); }); @@ -109,7 +109,7 @@ describe('zodResolver', () => { ); expectTypeOf(resolver).toEqualTypeOf< - Resolver + Resolver<{ id: number }, unknown, { id: string }> >(); }); @@ -145,7 +145,7 @@ describe('zodResolver', () => { it('should correctly infer the output type from a Zod schema when different input and output types are specified for the handleSubmit function in useForm', () => { const { handleSubmit } = useForm<{ id: string }, any, { id: boolean }>({ - resolver: zodResolver(z.object({ id: z.boolean() })), + resolver: zodResolver(z.object({ id: z.string().transform((s) => !!s) })), }); expectTypeOf(handleSubmit).parameter(0).toEqualTypeOf< @@ -154,4 +154,31 @@ describe('zodResolver', () => { }> >(); }); + + it('should correctly infer the output type from a zod schema using a transform with schemaOptions.raw', () => { + const resolver1 = zodResolver( + z.object({ id: z.number().transform((val) => String(val)) }), + undefined, + { raw: true }, + ); + expectTypeOf(resolver1).toEqualTypeOf< + Resolver<{ id: number }, unknown, { id: number }> + >(); + + const resolver2 = zodResolver( + z.object({ id: z.number().transform((val) => String(val)) }), + {}, + { raw: false }, + ); + expectTypeOf(resolver2).toEqualTypeOf< + Resolver<{ id: number }, unknown, { id: string }> + >(); + + const resolver3 = zodResolver( + z.object({ id: z.number().transform((val) => String(val)) }), + ); + expectTypeOf(resolver3).toEqualTypeOf< + Resolver<{ id: number }, unknown, { id: string }> + >(); + }); }); diff --git a/zod/src/zod.ts b/zod/src/zod.ts index bbd29070..14529706 100644 --- a/zod/src/zod.ts +++ b/zod/src/zod.ts @@ -1,9 +1,10 @@ import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers'; import { FieldError, - FieldErrors, FieldValues, Resolver, + ResolverError, + ResolverSuccess, appendErrors, } from 'react-hook-form'; import { ZodError, z } from 'zod'; @@ -80,17 +81,27 @@ function parseErrorSchema( * resolver: zodResolver(schema) * }); */ -export function zodResolver< - Input extends FieldValues, - Context, - Output, - Schema extends z.ZodSchema = z.ZodSchema< - Output, - any, - Input - >, ->( - schema: Schema, +// passing `resolverOptions.raw: false` (or omitting) you get the transformed output type +export function zodResolver( + schema: z.ZodSchema, + schemaOptions?: Partial, + resolverOptions?: { + mode?: 'async' | 'sync'; + raw?: false; + }, +): Resolver +// passing `resolverOptions.raw: true` you get back the input type +export function zodResolver( + schema: z.ZodSchema, + schemaOptions: Partial | undefined, + resolverOptions: { + mode?: 'async' | 'sync'; + raw: true; + }, +): Resolver +// implementation +export function zodResolver< Input extends FieldValues, Context, Output>( + schema: z.ZodSchema, schemaOptions?: Partial, resolverOptions: { mode?: 'async' | 'sync'; @@ -99,24 +110,20 @@ export function zodResolver< ): Resolver< Input, Context, - (typeof resolverOptions)['raw'] extends true - ? Input - : unknown extends Output - ? z.output - : Output + Output | Input // consumers never see this type; they only see types from overload signatures > { - return async (values, _, options) => { + return async (values: Input, _, options) => { try { - const data = await schema[ - resolverOptions.mode === 'sync' ? 'parse' : 'parseAsync' - ](values, schemaOptions); + const data = resolverOptions.mode === 'sync' + ? schema.parse(values, schemaOptions) + : await schema.parseAsync(values, schemaOptions); options.shouldUseNativeValidation && validateFieldsNatively({}, options); return { - errors: {} as FieldErrors, + errors: {}, values: resolverOptions.raw ? Object.assign({}, values) : data, - }; + } satisfies ResolverSuccess; } catch (error) { if (isZodError(error)) { return { @@ -129,7 +136,7 @@ export function zodResolver< ), options, ), - }; + } satisfies ResolverError; } throw error;