Skip to content

Commit a45a85e

Browse files
committed
typeschema
1 parent b49bc81 commit a45a85e

File tree

7 files changed

+138
-49
lines changed

7 files changed

+138
-49
lines changed

standard-schema/src/__tests__/standard-schema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ describe('standardSchemaResolver', () => {
116116

117117
const form = useForm({
118118
resolver: standardSchemaResolver(schema),
119+
defaultValues: {
120+
id: 3,
121+
},
119122
});
120123

121124
expectTypeOf(form.watch('id')).toEqualTypeOf<number>();
@@ -132,6 +135,9 @@ describe('standardSchemaResolver', () => {
132135

133136
const form = useForm({
134137
resolver: standardSchemaResolver(schema),
138+
defaultValues: {
139+
id: 3,
140+
},
135141
});
136142

137143
expectTypeOf(form.watch('id')).toEqualTypeOf<number>();

typeschema/src/__tests__/Form.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,16 @@ const schema = z.object({
1111
password: z.string().min(1, { message: 'password field is required' }),
1212
});
1313

14-
type FormData = Infer<typeof schema> & { unusedProperty: string };
15-
1614
interface Props {
17-
onSubmit: (data: FormData) => void;
15+
onSubmit: (data: Infer<typeof schema>) => void;
1816
}
1917

2018
function TestComponent({ onSubmit }: Props) {
2119
const {
2220
register,
2321
handleSubmit,
2422
formState: { errors },
25-
} = useForm<FormData>({
23+
} = useForm({
2624
resolver: typeschemaResolver(schema), // Useful to check TypeScript regressions
2725
});
2826

typeschema/src/__tests__/__fixtures__/data.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export const invalidData = {
6666
birthYear: 'birthYear',
6767
like: [{ id: 'z' }],
6868
url: 'abc',
69-
};
69+
} as unknown as z.input<typeof schema>;
7070

7171
export const fields: Record<InternalFieldName, Field['_f']> = {
7272
username: {

typeschema/src/__tests__/typeschema.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import * as typeschema from '@typeschema/main';
2+
import { Resolver, SubmitHandler, useForm } from 'react-hook-form';
3+
import { z } from 'zod';
24
import { typeschemaResolver } from '..';
35
import { fields, invalidData, schema, validData } from './__fixtures__/data';
46

@@ -67,4 +69,79 @@ describe('typeschemaResolver', () => {
6769

6870
await expect(promise).rejects.toThrow('custom error');
6971
});
72+
73+
/**
74+
* Type inference tests
75+
*/
76+
it('should correctly infer the output type from a typeschema schema', () => {
77+
const resolver = typeschemaResolver(z.object({ id: z.number() }));
78+
79+
expectTypeOf(resolver).toEqualTypeOf<
80+
Resolver<{ id: number }, unknown, { id: number }>
81+
>();
82+
});
83+
84+
it('should correctly infer the output type from a typeschema schema using a transform', () => {
85+
const resolver = typeschemaResolver(
86+
z.object({ id: z.number().transform((val) => String(val)) }),
87+
);
88+
89+
expectTypeOf(resolver).toEqualTypeOf<
90+
Resolver<{ id: number }, unknown, { id: string }>
91+
>();
92+
});
93+
94+
it('should correctly infer the output type from a typeschema schema when a different input type is specified', () => {
95+
const schema = z.object({ id: z.number() }).transform(({ id }) => {
96+
return { id: String(id) };
97+
});
98+
99+
const resolver = typeschemaResolver<
100+
{ id: number },
101+
any,
102+
z.output<typeof schema>
103+
>(schema);
104+
105+
expectTypeOf(resolver).toEqualTypeOf<
106+
Resolver<{ id: number }, any, { id: string }>
107+
>();
108+
});
109+
110+
it('should correctly infer the output type from a typeschema schema for the handleSubmit function in useForm', () => {
111+
const schema = z.object({ id: z.number() });
112+
113+
const form = useForm({
114+
resolver: typeschemaResolver(schema),
115+
defaultValues: {
116+
id: 3,
117+
},
118+
});
119+
120+
expectTypeOf(form.watch('id')).toEqualTypeOf<number>();
121+
122+
expectTypeOf(form.handleSubmit).parameter(0).toEqualTypeOf<
123+
SubmitHandler<{
124+
id: number;
125+
}>
126+
>();
127+
});
128+
129+
it('should correctly infer the output type from a typeschema schema with a transform for the handleSubmit function in useForm', () => {
130+
const schema = z.object({ id: z.number().transform((val) => String(val)) });
131+
132+
const form = useForm({
133+
resolver: typeschemaResolver(schema),
134+
defaultValues: {
135+
id: 3,
136+
},
137+
});
138+
139+
expectTypeOf(form.watch('id')).toEqualTypeOf<number>();
140+
141+
expectTypeOf(form.handleSubmit).parameter(0).toEqualTypeOf<
142+
SubmitHandler<{
143+
id: string;
144+
}>
145+
>();
146+
});
70147
});

typeschema/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
export * from './typeschema';
2-
export * from './types';

typeschema/src/types.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

typeschema/src/typeschema.ts

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers';
2-
import type { ValidationIssue } from '@typeschema/core';
3-
import { validate } from '@typeschema/main';
4-
import { FieldError, FieldErrors, appendErrors } from 'react-hook-form';
5-
import type { Resolver } from './types';
2+
import {
3+
FieldError,
4+
FieldErrors,
5+
FieldValues,
6+
Resolver,
7+
appendErrors,
8+
} from 'react-hook-form';
9+
import { StandardSchemaV1 } from 'zod/lib/standard-schema';
610

711
const parseErrorSchema = (
8-
typeschemaErrors: ValidationIssue[],
12+
typeschemaErrors: readonly StandardSchemaV1.Issue[],
913
validateAllFieldCriteria: boolean,
1014
): FieldErrors => {
15+
const schemaErrors = Object.assign([], typeschemaErrors);
1116
const errors: Record<string, FieldError> = {};
1217

13-
for (; typeschemaErrors.length; ) {
18+
for (; schemaErrors.length; ) {
1419
const error = typeschemaErrors[0];
1520

1621
if (!error.path) {
@@ -37,12 +42,28 @@ const parseErrorSchema = (
3742
) as FieldError;
3843
}
3944

40-
typeschemaErrors.shift();
45+
schemaErrors.shift();
4146
}
4247

4348
return errors;
4449
};
4550

51+
export function typeschemaResolver<Input extends FieldValues, Context, Output>(
52+
schema: StandardSchemaV1<Input, Output>,
53+
_schemaOptions?: never,
54+
resolverOptions?: {
55+
raw?: false;
56+
},
57+
): Resolver<Input, Context, Output>;
58+
59+
export function typeschemaResolver<Input extends FieldValues, Context, Output>(
60+
schema: StandardSchemaV1<Input, Output>,
61+
_schemaOptions: never | undefined,
62+
resolverOptions: {
63+
raw: true;
64+
},
65+
): Resolver<Input, Context, Input>;
66+
4667
/**
4768
* Creates a resolver for react-hook-form using TypeSchema validation
4869
* @param {any} schema - The TypeSchema to validate against
@@ -60,30 +81,36 @@ const parseErrorSchema = (
6081
* resolver: typeschemaResolver(schema)
6182
* });
6283
*/
63-
export const typeschemaResolver: Resolver =
64-
(schema, _, resolverOptions = {}) =>
65-
async (values, _, options) => {
66-
const result = await validate(schema, values);
84+
export function typeschemaResolver<Input extends FieldValues, Context, Output>(
85+
schema: StandardSchemaV1<Input, Output>,
86+
_schemaOptions?: never,
87+
resolverOptions: {
88+
raw?: boolean;
89+
} = {},
90+
): Resolver<Input, Context, Output | Input> {
91+
return async (values, _, options) => {
92+
let result = schema['~standard'].validate(values);
93+
if (result instanceof Promise) {
94+
result = await result;
95+
}
6796

68-
options.shouldUseNativeValidation && validateFieldsNatively({}, options);
97+
if (result.issues) {
98+
const errors = parseErrorSchema(
99+
result.issues,
100+
!options.shouldUseNativeValidation && options.criteriaMode === 'all',
101+
);
69102

70-
if (result.success) {
71103
return {
72-
errors: {} as FieldErrors,
73-
values: resolverOptions.raw
74-
? Object.assign({}, values)
75-
: (result.data as any),
104+
values: {},
105+
errors: toNestErrors(errors, options),
76106
};
77107
}
78108

109+
options.shouldUseNativeValidation && validateFieldsNatively({}, options);
110+
79111
return {
80-
values: {},
81-
errors: toNestErrors(
82-
parseErrorSchema(
83-
result.issues,
84-
!options.shouldUseNativeValidation && options.criteriaMode === 'all',
85-
),
86-
options,
87-
),
112+
values: resolverOptions.raw ? Object.assign({}, values) : result.value,
113+
errors: {},
88114
};
89115
};
116+
}

0 commit comments

Comments
 (0)