diff --git a/packages/form-core/src/formOptions.ts b/packages/form-core/src/formOptions.ts index e77460e6c..5fa45bfd6 100644 --- a/packages/form-core/src/formOptions.ts +++ b/packages/form-core/src/formOptions.ts @@ -1,4 +1,5 @@ import type { + AnyFormOptions, FormAsyncValidateOrFn, FormOptions, FormValidateOrFn, @@ -21,22 +22,7 @@ without losing the benefits from the TOptions generic. */ export function formOptions< - TOptions extends Partial< - FormOptions< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer, - TSubmitMeta - > - >, + TOptions, TFormData, TOnMount extends undefined | FormValidateOrFn, TOnChange extends undefined | FormValidateOrFn, @@ -48,23 +34,25 @@ export function formOptions< TOnDynamic extends undefined | FormValidateOrFn, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, TOnServer extends undefined | FormAsyncValidateOrFn, - TSubmitMeta = never, + /* + Defaulting this to never makes it no longer assignable to `AnyFieldApi` when using listeners for some reason. + Stick to the default `unknown` instead, as it will still prevent unsafe overwrites of `onSubmitMeta`. + */ + TSubmitMeta, >( - defaultOpts: Partial< - FormOptions< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer, - TSubmitMeta - > + defaultOpts: FormOptions< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta > & TOptions, ): TOptions { diff --git a/packages/form-core/tests/formOptions.test-d.ts b/packages/form-core/tests/formOptions.test-d.ts index eb7b39a45..d34496779 100644 --- a/packages/form-core/tests/formOptions.test-d.ts +++ b/packages/form-core/tests/formOptions.test-d.ts @@ -1,6 +1,10 @@ import { describe, expectTypeOf, it } from 'vitest' -import { FormApi, formOptions } from '../src/index' -import type { FormAsyncValidateOrFn, FormValidateOrFn } from '../src/index' +import { FieldApi, FormApi, formOptions } from '../src/index' +import type { + AnyFieldApi, + FormAsyncValidateOrFn, + FormValidateOrFn, +} from '../src/index' describe('formOptions', () => { it('types should be properly inferred', () => { @@ -197,7 +201,8 @@ describe('formOptions', () => { FormAsyncValidateOrFn | undefined, FormValidateOrFn | undefined, FormAsyncValidateOrFn | undefined, - FormAsyncValidateOrFn | undefined + FormAsyncValidateOrFn | undefined, + unknown > const formOpts = formOptions({ @@ -208,7 +213,7 @@ describe('formOptions', () => { listeners: { onSubmit: ({ formApi, meta }) => { expectTypeOf(formApi).toEqualTypeOf() - expectTypeOf(meta).toEqualTypeOf() + expectTypeOf(meta).toEqualTypeOf() }, }, }) @@ -323,4 +328,98 @@ describe('formOptions', () => { (undefined | 'Too short!' | 'I just need an error')[] >() }) + + it('should allow listeners', () => { + const options = formOptions({ + defaultValues: { name: '' }, + validators: { + onChange: () => 'Error', + }, + listeners: { + onChange: ({ formApi }) => { + expectTypeOf(formApi.state.values).toEqualTypeOf<{ name: string }>() + }, + }, + }) + }) + + it('should prevent overwriting onSubmitMeta if used', () => { + type FormData = { + firstName: string + lastName: string + } + type SubmitMeta = { bool: boolean } + + const optsWithUsedMeta = formOptions({ + defaultValues: { firstName: '', lastName: '' } as FormData, + onSubmitMeta: { bool: false } as SubmitMeta, + onSubmit: ({ meta }) => { + expectTypeOf(meta).toEqualTypeOf() + }, + }) + + const form1 = new FormApi(optsWithUsedMeta) + expectTypeOf(form1.handleSubmit).toBeCallableWith({ bool: true }) + const form2 = new FormApi({ + ...optsWithUsedMeta, + // @ts-expect-error cannot overwrite used submitMeta + onSubmitMeta: { change: 'value' }, + }) + expectTypeOf(form2.handleSubmit).toBeCallableWith({ bool: true }) + }) + + it('should allow overwriting onSubmitMeta if unused', () => { + type FormData = { + firstName: string + lastName: string + } + type SubmitMeta = { bool: boolean } + + const optsWithUnusedMeta = formOptions({ + defaultValues: { firstName: '', lastName: '' } as FormData, + onSubmitMeta: { bool: false } as SubmitMeta, + }) + + const form1 = new FormApi({ + ...optsWithUnusedMeta, + }) + + const form2 = new FormApi({ + ...optsWithUnusedMeta, + onSubmitMeta: { change: 'value' }, + onSubmit: ({ meta }) => { + expectTypeOf(meta).toEqualTypeOf<{ change: string }>() + }, + }) + + expectTypeOf(form1.handleSubmit).toBeCallableWith({ bool: true }) + // @ts-expect-error wrong meta shape + expectTypeOf(form2.handleSubmit).toBeCallableWith({ bool: true }) + expectTypeOf(form2.handleSubmit).toBeCallableWith({ change: 'test' }) + }) + + it('should allow assigning fields to be assignable to AnyFieldApi', () => { + const formOpts = formOptions({ + defaultValues: { firstName: '' }, + onSubmit: async ({ value }) => { + console.log(value) + }, + }) + + const form = new FormApi({ ...formOpts }) + const field = new FieldApi({ + form, + name: 'firstName', + validators: { + onChange: ({ value }) => + !value + ? 'A first name is required' + : value.length < 3 + ? 'First name must be at least 3 characters' + : undefined, + }, + }) + + expectTypeOf(field).toExtend() + }) })