From 885f1e8ef4bd7620b96b39e11cc2c430ad11af7e Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:00:29 +0100 Subject: [PATCH 1/3] test 1 --- packages/form-core/src/formOptions.ts | 46 +++++++++------------------ 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/packages/form-core/src/formOptions.ts b/packages/form-core/src/formOptions.ts index e77460e6c..4175aad4f 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, @@ -50,21 +36,19 @@ export function formOptions< TOnServer extends undefined | FormAsyncValidateOrFn, TSubmitMeta = never, >( - 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 { From d87dcf90098f93e9c3f5262fb156aa6388f2e2c5 Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:08:08 +0100 Subject: [PATCH 2/3] test 2 (fixing submitMeta) --- packages/form-core/src/formOptions.ts | 6 +++++- .../form-core/tests/formOptions.test-d.ts | 19 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/form-core/src/formOptions.ts b/packages/form-core/src/formOptions.ts index 4175aad4f..0c07168d4 100644 --- a/packages/form-core/src/formOptions.ts +++ b/packages/form-core/src/formOptions.ts @@ -34,7 +34,11 @@ export function formOptions< TOnDynamic extends undefined | FormValidateOrFn, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, TOnServer extends undefined | FormAsyncValidateOrFn, - TSubmitMeta = never, + /* + This is not guaranteed to be `never`. A user could spread + and add them later + */ + TSubmitMeta, >( defaultOpts: FormOptions< TFormData, diff --git a/packages/form-core/tests/formOptions.test-d.ts b/packages/form-core/tests/formOptions.test-d.ts index eb7b39a45..f1d7dbe90 100644 --- a/packages/form-core/tests/formOptions.test-d.ts +++ b/packages/form-core/tests/formOptions.test-d.ts @@ -197,7 +197,8 @@ describe('formOptions', () => { FormAsyncValidateOrFn | undefined, FormValidateOrFn | undefined, FormAsyncValidateOrFn | undefined, - FormAsyncValidateOrFn | undefined + FormAsyncValidateOrFn | undefined, + unknown > const formOpts = formOptions({ @@ -208,7 +209,7 @@ describe('formOptions', () => { listeners: { onSubmit: ({ formApi, meta }) => { expectTypeOf(formApi).toEqualTypeOf() - expectTypeOf(meta).toEqualTypeOf() + expectTypeOf(meta).toEqualTypeOf() }, }, }) @@ -323,4 +324,18 @@ 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 }>() + }, + }, + }) + }) }) From 5ee45668003fb22eac992297f26bdb6f31fd3aed Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:29:24 +0100 Subject: [PATCH 3/3] chore: add unit tests --- packages/form-core/src/formOptions.ts | 6 +- .../form-core/tests/formOptions.test-d.ts | 88 ++++++++++++++++++- 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/packages/form-core/src/formOptions.ts b/packages/form-core/src/formOptions.ts index 0c07168d4..5fa45bfd6 100644 --- a/packages/form-core/src/formOptions.ts +++ b/packages/form-core/src/formOptions.ts @@ -35,9 +35,9 @@ export function formOptions< TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, TOnServer extends undefined | FormAsyncValidateOrFn, /* - This is not guaranteed to be `never`. A user could spread - and add them later - */ + 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: FormOptions< diff --git a/packages/form-core/tests/formOptions.test-d.ts b/packages/form-core/tests/formOptions.test-d.ts index f1d7dbe90..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', () => { @@ -338,4 +342,84 @@ describe('formOptions', () => { }, }) }) + + 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() + }) })