@hookform/resolvers 6.0.1 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
- import { Type } from '@sinclair/typebox';
2
- import { TypeCompiler } from '@sinclair/typebox/compiler';
3
1
  import { renderHook } from '@testing-library/react';
4
2
  import { Resolver, SubmitHandler, useForm } from 'react-hook-form';
3
+ import { Type } from 'typebox';
4
+ import { Compile } from 'typebox/compile';
5
5
  import { typeboxResolver } from '..';
6
6
  import { fields, invalidData, schema, validData } from './__fixtures__/data';
7
7
 
@@ -47,8 +47,8 @@ describe('typeboxResolver', () => {
47
47
  >();
48
48
  });
49
49
 
50
- it('should correctly infer the output type from a typebox schema with TypeCompiler', () => {
51
- const typecheck = TypeCompiler.Compile(Type.Object({ id: Type.Number() }));
50
+ it('should correctly infer the output type from a typebox schema with Compile', () => {
51
+ const typecheck = Compile(Type.Object({ id: Type.Number() }));
52
52
  const resolver = typeboxResolver(typecheck);
53
53
 
54
54
  expectTypeOf(resolver).toEqualTypeOf<
@@ -56,10 +56,38 @@ describe('typeboxResolver', () => {
56
56
  >();
57
57
  });
58
58
 
59
+ it('should correctly infer the output type from a typebox schema with Compile and a reference', () => {
60
+ const typecheck = Compile(
61
+ { Name: Type.String() },
62
+ Type.Object({ name: Type.Ref('Name') }),
63
+ );
64
+ const resolver = typeboxResolver(typecheck);
65
+
66
+ expectTypeOf(resolver).toEqualTypeOf<
67
+ Resolver<{ name: string }, unknown, { name: string }>
68
+ >();
69
+ });
70
+
71
+ it('should correctly infer the output type from a typebox schema with Compile and a reference using a transform', () => {
72
+ const typecheck = Compile(
73
+ {
74
+ Id: Type.Codec(Type.Number())
75
+ .Decode((v) => String(v))
76
+ .Encode((v) => Number.parseInt(v)),
77
+ },
78
+ Type.Object({ id: Type.Ref('Id') }),
79
+ );
80
+ const resolver = typeboxResolver(typecheck);
81
+
82
+ expectTypeOf(resolver).toEqualTypeOf<
83
+ Resolver<{ id: number }, unknown, { id: string }>
84
+ >();
85
+ });
86
+
59
87
  it('should correctly infer the output type from a typebox schema using a transform', () => {
60
88
  const resolver = typeboxResolver(
61
89
  Type.Object({
62
- id: Type.Transform(Type.Number())
90
+ id: Type.Codec(Type.Number())
63
91
  .Decode((v) => String(v))
64
92
  .Encode((v) => Number.parseInt(v)),
65
93
  }),
@@ -93,8 +121,8 @@ describe('typeboxResolver', () => {
93
121
  >();
94
122
  });
95
123
 
96
- it('should correctly infer the output type from a typebox schema with TypeCompiler for the handleSubmit function in useForm', () => {
97
- const typecheck = TypeCompiler.Compile(Type.Object({ id: Type.Number() }));
124
+ it('should correctly infer the output type from a typebox schema with Compile for the handleSubmit function in useForm', () => {
125
+ const typecheck = Compile(Type.Object({ id: Type.Number() }));
98
126
 
99
127
  const {
100
128
  result: { current: form },
@@ -118,7 +146,7 @@ describe('typeboxResolver', () => {
118
146
 
119
147
  it('should correctly infer the output type from a typebox schema with a transform for the handleSubmit function in useForm', () => {
120
148
  const schema = Type.Object({
121
- id: Type.Transform(Type.Number())
149
+ id: Type.Codec(Type.Number())
122
150
  .Decode((v) => String(v))
123
151
  .Encode((v) => Number.parseInt(v)),
124
152
  });
@@ -1,40 +1,63 @@
1
1
  import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers';
2
- import { Static, StaticDecode, TObject } from '@sinclair/typebox';
3
- import { TypeCheck } from '@sinclair/typebox/compiler';
4
- import { Value, type ValueError } from '@sinclair/typebox/value';
5
2
  import { FieldError, Resolver, appendErrors } from 'react-hook-form';
3
+ import type { Static, StaticDecode, TObject, TProperties } from 'typebox';
4
+ import { Validator } from 'typebox/compile';
5
+ import type { TLocalizedValidationError } from 'typebox/error';
6
+ import { Locale } from 'typebox/system';
7
+ import { Value } from 'typebox/value';
8
+
9
+ // TypeBox reports a single `required` error on the parent object listing every
10
+ // missing property; split it into one error per missing field so each one lands
11
+ // on its own path, re-localizing the message for that single property.
12
+ function expandRequiredErrors(
13
+ errors: TLocalizedValidationError[],
14
+ ): TLocalizedValidationError[] {
15
+ return errors.flatMap<TLocalizedValidationError>((error) =>
16
+ error.keyword === 'required'
17
+ ? error.params.requiredProperties.map((property) => {
18
+ const single = {
19
+ ...error,
20
+ instancePath: `${error.instancePath}/${property.replace(/~/g, '~0').replace(/\//g, '~1')}`,
21
+ params: { requiredProperties: [property] },
22
+ };
23
+ return { ...single, message: Locale.Get()(single) };
24
+ })
25
+ : [error],
26
+ );
27
+ }
6
28
 
7
29
  function parseErrorSchema(
8
- _errors: ValueError[],
30
+ _errors: TLocalizedValidationError[],
9
31
  validateAllFieldCriteria: boolean,
10
32
  ) {
11
33
  // See the matching comment in the zod resolver's `parseZod3Issues`.
12
34
  const errors: Record<string, FieldError> = Object.create(null);
13
- for (; _errors.length; ) {
14
- const error = _errors[0];
15
- const { type, message, path } = error;
16
- const _path = path.substring(1).replace(/\//g, '.');
35
+ for (const error of _errors) {
36
+ const { keyword: type, message, instancePath } = error;
37
+ const _path = instancePath
38
+ .split('/')
39
+ .slice(1)
40
+ .map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'))
41
+ .join('.');
17
42
 
18
43
  if (!errors[_path]) {
19
- errors[_path] = { message, type: '' + type };
44
+ errors[_path] = { message, type };
20
45
  }
21
46
 
22
47
  if (validateAllFieldCriteria) {
23
48
  const types = errors[_path].types;
24
- const messages = types && types['' + type];
49
+ const messages = types && types[type];
25
50
 
26
51
  errors[_path] = appendErrors(
27
52
  _path,
28
53
  validateAllFieldCriteria,
29
54
  errors,
30
- '' + type,
55
+ type,
31
56
  messages
32
- ? ([] as string[]).concat(messages as string[], error.message)
33
- : error.message,
57
+ ? ([] as string[]).concat(messages as string[], message)
58
+ : message,
34
59
  ) as FieldError;
35
60
  }
36
-
37
- _errors.shift();
38
61
  }
39
62
 
40
63
  return errors;
@@ -42,10 +65,10 @@ function parseErrorSchema(
42
65
 
43
66
  /**
44
67
  * Creates a resolver for react-hook-form using Typebox schema validation
45
- * @param {Schema | TypeCheck<Schema>} schema - The Typebox schema to validate against
68
+ * @param {Schema | Validator<TypeContext, Schema>} schema - The Typebox schema to validate against
46
69
  * @param {Object} options - Additional resolver configuration
47
70
  * @param {string} [options.mode='async'] - Validation mode
48
- * @returns {Resolver<Static<Schema>>} A resolver function compatible with react-hook-form
71
+ * @returns {Resolver<Static<Schema, TypeContext>>} A resolver function compatible with react-hook-form
49
72
  * @example
50
73
  * const schema = Type.Object({
51
74
  * name: Type.String(),
@@ -56,18 +79,25 @@ function parseErrorSchema(
56
79
  * resolver: typeboxResolver(schema)
57
80
  * });
58
81
  *
59
- * // Schemas using a custom/third-party `Kind` (e.g. ElysiaJS's `t.Files()`) must have
60
- * // that kind registered with TypeBox's own `TypeRegistry` before validating, since
61
- * // this resolver validates via `@sinclair/typebox`'s `Value.Errors`/`Value.Check`:
62
- * // TypeRegistry.Set('Files', (schema, value) => Array.isArray(value));
82
+ * // Custom string formats must be registered with TypeBox's own `Format` registry
83
+ * // before validating, since this resolver validates via `typebox`'s `Value.Errors`:
84
+ * // Format.Set('slug', (value) => /^[a-z0-9-]+$/.test(value));
63
85
  */
64
- export function typeboxResolver<Schema extends TObject, Context>(
65
- schema: Schema | TypeCheck<Schema>,
66
- ): Resolver<Static<Schema>, Context, StaticDecode<Schema>> {
67
- return async (values: Static<Schema>, _, options) => {
68
- const errors = Array.from(
69
- schema instanceof TypeCheck
70
- ? schema.Errors(values)
86
+ export function typeboxResolver<
87
+ Schema extends TObject,
88
+ Context,
89
+ TypeContext extends TProperties = Record<never, never>,
90
+ >(
91
+ schema: Schema | Validator<TypeContext, Schema>,
92
+ ): Resolver<
93
+ Static<Schema, TypeContext>,
94
+ Context,
95
+ StaticDecode<Schema, TypeContext>
96
+ > {
97
+ return async (values: Static<Schema, TypeContext>, _, options) => {
98
+ const errors = expandRequiredErrors(
99
+ schema instanceof Validator
100
+ ? (schema as Validator<TypeContext, Schema>).Errors(values)
71
101
  : Value.Errors(schema, values),
72
102
  );
73
103
 
@@ -76,7 +106,7 @@ export function typeboxResolver<Schema extends TObject, Context>(
76
106
  if (!errors.length) {
77
107
  return {
78
108
  errors: {},
79
- values,
109
+ values: values as StaticDecode<Schema, TypeContext>,
80
110
  };
81
111
  }
82
112