From 98105651e6cc9bedf2424b474d5bd329726ab6ef Mon Sep 17 00:00:00 2001 From: Leonardo Montini Date: Tue, 6 May 2025 22:37:39 +0200 Subject: [PATCH] fix: only allow setting manual errors according to defined validators --- .../src/index.tsx | 3 +- packages/form-core/src/FieldApi.ts | 29 +++++---- packages/form-core/src/FormApi.ts | 32 +++++----- packages/form-core/tests/FieldApi.spec.ts | 20 ++---- packages/form-core/tests/FieldApi.test-d.ts | 64 +++++++++++++++++++ packages/form-core/tests/FormApi.spec.ts | 2 +- packages/form-core/tests/FormApi.test-d.ts | 43 +++++++++++++ 7 files changed, 147 insertions(+), 46 deletions(-) diff --git a/examples/react/field-errors-from-form-validators/src/index.tsx b/examples/react/field-errors-from-form-validators/src/index.tsx index ace86f45b..bd200068d 100644 --- a/examples/react/field-errors-from-form-validators/src/index.tsx +++ b/examples/react/field-errors-from-form-validators/src/index.tsx @@ -112,8 +112,7 @@ export default function App() { errorMap.onSubmit ? (
- There was an error on the form:{' '} - {errorMap.onSubmit?.toString()} + There was an error on the form: {errorMap.onSubmit.toString()}
) : null diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 7624b7c7a..d7aeb309c 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1671,17 +1671,24 @@ export class FieldApi< /** * Updates the field's errorMap */ - setErrorMap(errorMap: ValidationErrorMap) { - this.setMeta( - (prev) => - ({ - ...prev, - errorMap: { - ...prev.errorMap, - ...errorMap, - }, - }) as never, - ) + setErrorMap( + errorMap: ValidationErrorMap< + UnwrapFieldValidateOrFn, + UnwrapFieldValidateOrFn, + UnwrapFieldAsyncValidateOrFn, + UnwrapFieldValidateOrFn, + UnwrapFieldAsyncValidateOrFn, + UnwrapFieldValidateOrFn, + UnwrapFieldAsyncValidateOrFn + >, + ) { + this.setMeta((prev) => ({ + ...prev, + errorMap: { + ...prev.errorMap, + ...errorMap, + }, + })) } /** diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index cfd292e46..2181daa25 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -2123,25 +2123,23 @@ export class FormApi< */ setErrorMap( errorMap: ValidationErrorMap< - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync + UnwrapFormValidateOrFn, + UnwrapFormValidateOrFn, + UnwrapFormAsyncValidateOrFn, + UnwrapFormValidateOrFn, + UnwrapFormAsyncValidateOrFn, + UnwrapFormValidateOrFn, + UnwrapFormAsyncValidateOrFn, + UnwrapFormAsyncValidateOrFn >, ) { - this.baseStore.setState( - (prev) => - ({ - ...prev, - errorMap: { - ...prev.errorMap, - ...errorMap, - }, - }) as never, - ) + this.baseStore.setState((prev) => ({ + ...prev, + errorMap: { + ...prev.errorMap, + ...errorMap, + }, + })) } /** diff --git a/packages/form-core/tests/FieldApi.spec.ts b/packages/form-core/tests/FieldApi.spec.ts index 11fe57c5a..01cd7eb77 100644 --- a/packages/form-core/tests/FieldApi.spec.ts +++ b/packages/form-core/tests/FieldApi.spec.ts @@ -1785,9 +1785,7 @@ describe('field api', () => { name: 'name', }) nameField.mount() - nameField.setErrorMap({ - onChange: "name can't be Josh", - }) + nameField.setErrorMap({ onChange: "name can't be Josh" as never }) expect(nameField.getMeta().isValid).toBe(false) expect(nameField.getMeta().errorMap.onChange).toEqual("name can't be Josh") }) @@ -1802,14 +1800,10 @@ describe('field api', () => { name: 'name', }) nameField.mount() - nameField.setErrorMap({ - onChange: "name can't be Josh", - }) + nameField.setErrorMap({ onChange: "name can't be Josh" as never }) expect(nameField.getMeta().isValid).toBe(false) expect(nameField.getMeta().errorMap.onChange).toEqual("name can't be Josh") - nameField.setErrorMap({ - onBlur: 'name must begin with uppercase', - }) + nameField.setErrorMap({ onBlur: 'name must begin with uppercase' as never }) expect(nameField.getMeta().isValid).toBe(false) expect(nameField.getMeta().errorMap.onChange).toEqual("name can't be Josh") expect(nameField.getMeta().errorMap.onBlur).toEqual( @@ -1827,14 +1821,10 @@ describe('field api', () => { name: 'name', }) nameField.mount() - nameField.setErrorMap({ - onChange: "name can't be Josh", - }) + nameField.setErrorMap({ onChange: "name can't be Josh" as never }) expect(nameField.getMeta().isValid).toBe(false) expect(nameField.getMeta().errorMap.onChange).toEqual("name can't be Josh") - nameField.setErrorMap({ - onChange: 'other validation error', - }) + nameField.setErrorMap({ onChange: 'other validation error' as never }) expect(nameField.getMeta().errorMap.onChange).toEqual( 'other validation error', ) diff --git a/packages/form-core/tests/FieldApi.test-d.ts b/packages/form-core/tests/FieldApi.test-d.ts index 97af6d5fd..9c67a8297 100644 --- a/packages/form-core/tests/FieldApi.test-d.ts +++ b/packages/form-core/tests/FieldApi.test-d.ts @@ -394,3 +394,67 @@ it('should only have field-level error types returned from parseValueWithSchema Promise >() }) + +it("should allow setting manual errors according to the validator's return type", () => { + const form = new FormApi({ + defaultValues: { + firstName: '', + lastName: '', + }, + validators: { + onChange: () => { + return { + fields: { + firstName: '123' as const, + }, + } + }, + }, + }) + + const field = new FieldApi({ + form, + name: 'firstName', + validators: { + onChange: () => 10 as const, + onBlur: () => ['onBlur'] as const, + }, + }) + + field.setErrorMap({ + onChange: '123', + }) + + expectTypeOf(field.setErrorMap).parameter(0).toEqualTypeOf<{ + onMount: undefined + onChange: '123' | 10 | undefined + onBlur: readonly ['onBlur'] | undefined + onSubmit: undefined + onServer: unknown + }> +}) + +it('should allow setting manual errors with standard schema validators on the field level', () => { + const form = new FormApi({ + defaultValues: { + firstName: '', + lastName: '', + }, + }) + + const field = new FieldApi({ + form, + name: 'firstName', + validators: { + onChange: z.string(), + }, + }) + + expectTypeOf(field.setErrorMap).parameter(0).toEqualTypeOf<{ + onMount: undefined + onChange: { message: string }[] | undefined + onBlur: undefined + onSubmit: undefined + onServer: unknown + }> +}) diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index ac151676a..292fce077 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -736,7 +736,7 @@ describe('form api', () => { field2.mount() field1.handleBlur() - field1.setErrorMap({ onSubmit: 'test' }) + field1.setErrorMap({ onSubmit: 'test' as never }) expect(field0.state.meta.isBlurred).toBe(false) expect(field1.state.meta.isBlurred).toBe(true) diff --git a/packages/form-core/tests/FormApi.test-d.ts b/packages/form-core/tests/FormApi.test-d.ts index eb8e4b619..87920ae71 100644 --- a/packages/form-core/tests/FormApi.test-d.ts +++ b/packages/form-core/tests/FormApi.test-d.ts @@ -116,3 +116,46 @@ it('should only have form-level error types returned from parseFieldValuesWithSc Promise >() }) + +it("should allow setting manual errors according to the validator's return type", () => { + const form = new FormApi({ + defaultValues: { + firstName: '', + lastName: '', + }, + validators: { + onChange: () => ['onChange'] as const, + onMount: () => 10 as const, + onBlur: () => ({ onBlur: true as const, onBlurNumber: 1 }), + onSubmit: () => 'onSubmit' as const, + onBlurAsync: () => Promise.resolve('onBlurAsync' as const), + onChangeAsync: () => Promise.resolve('onChangeAsync' as const), + onSubmitAsync: () => Promise.resolve('onSubmitAsync' as const), + }, + }) + + expectTypeOf(form.setErrorMap).parameter(0).toEqualTypeOf<{ + onMount: 10 | undefined + onChange: readonly ['onChange'] | 'onChangeAsync' | undefined + onBlur: { onBlur: true; onBlurNumber: number } | 'onBlurAsync' | undefined + onSubmit: 'onSubmit' | 'onSubmitAsync' | undefined + onServer: undefined + }> +}) + +it('should not allow setting manual errors if no validator is specified', () => { + const form = new FormApi({ + defaultValues: { + firstName: '', + lastName: '', + }, + }) + + expectTypeOf(form.setErrorMap).parameter(0).toEqualTypeOf<{ + onMount: undefined + onChange: undefined + onBlur: undefined + onSubmit: undefined + onServer: undefined + }> +})