@hookform/resolvers 6.0.2 → 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,44 +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
35
+ for (const error of _errors) {
36
+ const { keyword: type, message, instancePath } = error;
37
+ const _path = instancePath
17
38
  .split('/')
18
39
  .slice(1)
19
40
  .map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'))
20
41
  .join('.');
21
42
 
22
43
  if (!errors[_path]) {
23
- errors[_path] = { message, type: '' + type };
44
+ errors[_path] = { message, type };
24
45
  }
25
46
 
26
47
  if (validateAllFieldCriteria) {
27
48
  const types = errors[_path].types;
28
- const messages = types && types['' + type];
49
+ const messages = types && types[type];
29
50
 
30
51
  errors[_path] = appendErrors(
31
52
  _path,
32
53
  validateAllFieldCriteria,
33
54
  errors,
34
- '' + type,
55
+ type,
35
56
  messages
36
- ? ([] as string[]).concat(messages as string[], error.message)
37
- : error.message,
57
+ ? ([] as string[]).concat(messages as string[], message)
58
+ : message,
38
59
  ) as FieldError;
39
60
  }
40
-
41
- _errors.shift();
42
61
  }
43
62
 
44
63
  return errors;
@@ -46,10 +65,10 @@ function parseErrorSchema(
46
65
 
47
66
  /**
48
67
  * Creates a resolver for react-hook-form using Typebox schema validation
49
- * @param {Schema | TypeCheck<Schema>} schema - The Typebox schema to validate against
68
+ * @param {Schema | Validator<TypeContext, Schema>} schema - The Typebox schema to validate against
50
69
  * @param {Object} options - Additional resolver configuration
51
70
  * @param {string} [options.mode='async'] - Validation mode
52
- * @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
53
72
  * @example
54
73
  * const schema = Type.Object({
55
74
  * name: Type.String(),
@@ -60,18 +79,25 @@ function parseErrorSchema(
60
79
  * resolver: typeboxResolver(schema)
61
80
  * });
62
81
  *
63
- * // Schemas using a custom/third-party `Kind` (e.g. ElysiaJS's `t.Files()`) must have
64
- * // that kind registered with TypeBox's own `TypeRegistry` before validating, since
65
- * // this resolver validates via `@sinclair/typebox`'s `Value.Errors`/`Value.Check`:
66
- * // 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));
67
85
  */
68
- export function typeboxResolver<Schema extends TObject, Context>(
69
- schema: Schema | TypeCheck<Schema>,
70
- ): Resolver<Static<Schema>, Context, StaticDecode<Schema>> {
71
- return async (values: Static<Schema>, _, options) => {
72
- const errors = Array.from(
73
- schema instanceof TypeCheck
74
- ? 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)
75
101
  : Value.Errors(schema, values),
76
102
  );
77
103
 
@@ -80,7 +106,7 @@ export function typeboxResolver<Schema extends TObject, Context>(
80
106
  if (!errors.length) {
81
107
  return {
82
108
  errors: {},
83
- values,
109
+ values: values as StaticDecode<Schema, TypeContext>,
84
110
  };
85
111
  }
86
112