Skip to content

Commit c6da66a

Browse files
committed
feat(arktype): type inference
1 parent c0fdb95 commit c6da66a

File tree

8 files changed

+124
-57
lines changed

8 files changed

+124
-57
lines changed

arktype/src/__tests__/Form.tsx

-28
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ const schema = type({
1010
password: 'string>1',
1111
});
1212

13-
type FormData = typeof schema.infer & { unusedProperty: string };
14-
1513
function TestComponent({
1614
onSubmit,
1715
}: {
@@ -54,29 +52,3 @@ test("form's validation with arkType and TypeScript's integration", async () =>
5452
).toBeInTheDocument();
5553
expect(handleSubmit).not.toHaveBeenCalled();
5654
});
57-
58-
export function TestComponentManualType({
59-
onSubmit,
60-
}: {
61-
onSubmit: (data: FormData) => void;
62-
}) {
63-
const {
64-
register,
65-
handleSubmit,
66-
formState: { errors },
67-
} = useForm<typeof schema.infer, undefined, FormData>({
68-
resolver: arktypeResolver(schema), // Useful to check TypeScript regressions
69-
});
70-
71-
return (
72-
<form onSubmit={handleSubmit(onSubmit)}>
73-
<input {...register('username')} />
74-
{errors.username && <span role="alert">{errors.username.message}</span>}
75-
76-
<input {...register('password')} />
77-
{errors.password && <span role="alert">{errors.password.message}</span>}
78-
79-
<button type="submit">submit</button>
80-
</form>
81-
);
82-
}

arktype/src/__tests__/arktype.ts

+56
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { type } from 'arktype';
2+
import { Resolver, useForm } from 'react-hook-form';
3+
import { SubmitHandler } from 'react-hook-form';
14
import { arktypeResolver } from '..';
25
import { fields, invalidData, schema, validData } from './__fixtures__/data';
36

@@ -23,4 +26,57 @@ describe('arktypeResolver', () => {
2326

2427
expect(result).toMatchSnapshot();
2528
});
29+
30+
/**
31+
* Type inference tests
32+
*/
33+
it('should correctly infer the output type from a arktype schema', () => {
34+
const resolver = arktypeResolver(type({ id: 'number' }));
35+
36+
expectTypeOf(resolver).toEqualTypeOf<
37+
Resolver<{ id: number }, unknown, { id: number }>
38+
>();
39+
});
40+
41+
it('should correctly infer the output type from a arktype schema using a transform', () => {
42+
const resolver = arktypeResolver(
43+
type({ id: type('string').pipe((s) => Number.parseInt(s)) }),
44+
);
45+
46+
expectTypeOf(resolver).toEqualTypeOf<
47+
Resolver<{ id: string }, unknown, { id: number }>
48+
>();
49+
});
50+
51+
it('should correctly infer the output type from a arktype schema for the handleSubmit function in useForm', () => {
52+
const schema = type({ id: 'number' });
53+
54+
const form = useForm({
55+
resolver: arktypeResolver(schema),
56+
});
57+
58+
expectTypeOf(form.watch('id')).toEqualTypeOf<number>();
59+
60+
expectTypeOf(form.handleSubmit).parameter(0).toEqualTypeOf<
61+
SubmitHandler<{
62+
id: number;
63+
}>
64+
>();
65+
});
66+
67+
it('should correctly infer the output type from a arktype schema with a transform for the handleSubmit function in useForm', () => {
68+
const schema = type({ id: type('string').pipe((s) => Number.parseInt(s)) });
69+
70+
const form = useForm({
71+
resolver: arktypeResolver(schema),
72+
});
73+
74+
expectTypeOf(form.watch('id')).toEqualTypeOf<string>();
75+
76+
expectTypeOf(form.handleSubmit).parameter(0).toEqualTypeOf<
77+
SubmitHandler<{
78+
id: number;
79+
}>
80+
>();
81+
});
2682
});

arktype/src/arktype.ts

+58-22
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,53 @@
11
import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers';
2-
import { ArkErrors, Type } from 'arktype';
3-
import { FieldError, FieldErrors, Resolver } from 'react-hook-form';
2+
import { StandardSchemaV1 } from '@standard-schema/spec';
3+
import { getDotPath } from '@standard-schema/utils';
4+
import { FieldError, FieldValues, Resolver } from 'react-hook-form';
45

5-
function parseErrorSchema(arkErrors: ArkErrors): Record<string, FieldError> {
6-
const errors = [...arkErrors];
7-
const fieldsErrors: Record<string, FieldError> = {};
6+
function parseErrorSchema(
7+
issues: readonly StandardSchemaV1.Issue[],
8+
validateAllFieldCriteria: boolean,
9+
) {
10+
const errors: Record<string, FieldError> = {};
811

9-
for (; errors.length; ) {
10-
const error = errors[0];
11-
const _path = error.path.join('.');
12+
for (let i = 0; i < issues.length; i++) {
13+
const error = issues[i];
14+
const path = getDotPath(error);
1215

13-
if (!fieldsErrors[_path]) {
14-
fieldsErrors[_path] = { message: error.message, type: error.code };
15-
}
16+
if (path) {
17+
if (!errors[path]) {
18+
errors[path] = { message: error.message, type: '' };
19+
}
20+
21+
if (validateAllFieldCriteria) {
22+
const types = errors[path].types || {};
1623

17-
errors.shift();
24+
errors[path].types = {
25+
...types,
26+
[Object.keys(types).length]: error.message,
27+
};
28+
}
29+
}
1830
}
1931

20-
return fieldsErrors;
32+
return errors;
2133
}
2234

35+
export function arktypeResolver<Input extends FieldValues, Context, Output>(
36+
schema: StandardSchemaV1<Input, Output>,
37+
_schemaOptions?: never,
38+
resolverOptions?: {
39+
raw?: false;
40+
},
41+
): Resolver<Input, Context, Output>;
42+
43+
export function arktypeResolver<Input extends FieldValues, Context, Output>(
44+
schema: StandardSchemaV1<Input, Output>,
45+
_schemaOptions: never | undefined,
46+
resolverOptions: {
47+
raw: true;
48+
},
49+
): Resolver<Input, Context, Input>;
50+
2351
/**
2452
* Creates a resolver for react-hook-form using Arktype schema validation
2553
* @param {Schema} schema - The Arktype schema to validate against
@@ -35,28 +63,36 @@ function parseErrorSchema(arkErrors: ArkErrors): Record<string, FieldError> {
3563
* resolver: arktypeResolver(schema)
3664
* });
3765
*/
38-
export function arktypeResolver<Schema extends Type<any, any>>(
39-
schema: Schema,
66+
export function arktypeResolver<Input extends FieldValues, Context, Output>(
67+
schema: StandardSchemaV1<Input, Output>,
4068
_schemaOptions?: never,
4169
resolverOptions: {
4270
raw?: boolean;
4371
} = {},
44-
): Resolver<Schema['inferOut']> {
45-
return (values, _, options) => {
46-
const out = schema(values);
72+
): Resolver<Input, Context, Input | Output> {
73+
return async (values: Input, _, options) => {
74+
let result = schema['~standard'].validate(values);
75+
if (result instanceof Promise) {
76+
result = await result;
77+
}
78+
79+
if (result.issues) {
80+
const errors = parseErrorSchema(
81+
result.issues,
82+
!options.shouldUseNativeValidation && options.criteriaMode === 'all',
83+
);
4784

48-
if (out instanceof ArkErrors) {
4985
return {
5086
values: {},
51-
errors: toNestErrors(parseErrorSchema(out), options),
87+
errors: toNestErrors(errors, options),
5288
};
5389
}
5490

5591
options.shouldUseNativeValidation && validateFieldsNatively({}, options);
5692

5793
return {
58-
errors: {} as FieldErrors,
59-
values: resolverOptions.raw ? Object.assign({}, values) : out,
94+
values: resolverOptions.raw ? Object.assign({}, values) : result.value,
95+
errors: {},
6096
};
6197
};
6298
}

computed-types/src/__tests__/computed-types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import Schema, { number } from 'computed-types';
12
import { Resolver, SubmitHandler, useForm } from 'react-hook-form';
23
import { computedTypesResolver } from '..';
34
import { fields, invalidData, schema, validData } from './__fixtures__/data';
4-
import Schema, { number } from 'computed-types';
55

66
const shouldUseNativeValidation = false;
77

effect-ts/src/__tests__/effect-ts.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Schema } from 'effect';
2-
import { effectTsResolver } from '..';
3-
import { fields, invalidData, schema, validData } from './__fixtures__/data';
42
import { Resolver, useForm } from 'react-hook-form';
53
import { SubmitHandler } from 'react-hook-form';
4+
import { effectTsResolver } from '..';
5+
import { fields, invalidData, schema, validData } from './__fixtures__/data';
66

77
const shouldUseNativeValidation = false;
88

io-ts/src/__tests__/io-ts.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import * as t from 'io-ts';
2+
import * as tt from 'io-ts-types';
13
import { Resolver, SubmitHandler, useForm } from 'react-hook-form';
24
import { ioTsResolver } from '..';
35
import { fields, invalidData, schema, validData } from './__fixtures__/data';
4-
import * as t from 'io-ts';
5-
import * as tt from 'io-ts-types';
66

77
const shouldUseNativeValidation = false;
88

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Resolver, SubmitHandler, useForm } from 'react-hook-form';
2+
import { z } from 'zod';
23
import { standardSchemaResolver } from '..';
34
import {
45
customSchema,
@@ -7,7 +8,6 @@ import {
78
schema,
89
validData,
910
} from './__fixtures__/data';
10-
import { z } from 'zod';
1111

1212
const shouldUseNativeValidation = false;
1313

@@ -51,7 +51,7 @@ describe('standardSchemaResolver', () => {
5151
it('should return values from standardSchemaResolver when validation pass & raw=true', async () => {
5252
const validateSpy = vi.spyOn(schema['~standard'], 'validate');
5353

54-
const result = await standardSchemaResolver(schema, {
54+
const result = await standardSchemaResolver(schema, undefined, {
5555
raw: true,
5656
})(validData, undefined, {
5757
fields,

standard-schema/src/standard-schema.ts

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export function standardSchemaResolver<
3838
Output,
3939
>(
4040
schema: StandardSchemaV1<Input, Output>,
41+
_schemaOptions?: never,
4142
resolverOptions?: {
4243
raw?: false;
4344
},
@@ -49,6 +50,7 @@ export function standardSchemaResolver<
4950
Output,
5051
>(
5152
schema: StandardSchemaV1<Input, Output>,
53+
_schemaOptions: never | undefined,
5254
resolverOptions: {
5355
raw: true;
5456
},
@@ -80,6 +82,7 @@ export function standardSchemaResolver<
8082
Output,
8183
>(
8284
schema: StandardSchemaV1<Input, Output>,
85+
_schemaOptions?: never,
8386
resolverOptions: {
8487
raw?: boolean;
8588
} = {},

0 commit comments

Comments
 (0)