Skip to content

Commit 628c5ef

Browse files
a-is-4-adamAdam NicholsonLeCarbonatorautofix-ci[bot]
authored
fix(form-core): infer formOptions parameter's type correctly (#1679)
* fix(form-core): improve formOptions type preservation - WIP - Add TFormData generic parameter to formOptions function - Add test to verify listener type preservation with formOptions - Current implementation has intentional type issues for investigation * move tests to correct file, fix imports * fix(form-core): use generic for formOptions parameter instead * ci: apply automated fixes and generate docs * chore: add intersection to formOptions parameter for type inference --------- Co-authored-by: Adam Nicholson <adam.nicholson@flightcentre.com> Co-authored-by: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent fc454d4 commit 628c5ef

File tree

2 files changed

+130
-5
lines changed

2 files changed

+130
-5
lines changed

packages/form-core/src/formOptions.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,62 @@
1-
import type { FormOptions } from './FormApi'
1+
import type {
2+
FormAsyncValidateOrFn,
3+
FormOptions,
4+
FormValidateOrFn,
5+
} from './FormApi'
6+
7+
/*
8+
9+
These types need to do two things:
10+
11+
1. Validator generics need to depend on the TFormData generic
12+
2. The resulting needs to allow overriding values
13+
14+
The generics from formOptions almost work, except that it loses information
15+
about how to infer TFormData.
16+
If you pass a validator function, it tries to resolve the `formApi` or `value`
17+
inside of it, meaning that TFormData changes to `unknown`.
18+
19+
To bypass this, the intersection for defaultOpts gives TypeScript that information again,
20+
without losing the benefits from the TOptions generic.
21+
*/
222

323
export function formOptions<
4-
T extends Partial<
5-
FormOptions<any, any, any, any, any, any, any, any, any, any, any, any>
24+
TOptions extends Partial<
25+
FormOptions<
26+
TFormData,
27+
undefined | FormValidateOrFn<TFormData>,
28+
undefined | FormValidateOrFn<TFormData>,
29+
undefined | FormAsyncValidateOrFn<TFormData>,
30+
undefined | FormValidateOrFn<TFormData>,
31+
undefined | FormAsyncValidateOrFn<TFormData>,
32+
undefined | FormValidateOrFn<TFormData>,
33+
undefined | FormAsyncValidateOrFn<TFormData>,
34+
undefined | FormValidateOrFn<TFormData>,
35+
undefined | FormAsyncValidateOrFn<TFormData>,
36+
undefined | FormAsyncValidateOrFn<TFormData>,
37+
TSubmitMeta
38+
>
639
>,
7-
>(defaultOpts: T) {
40+
TFormData = TOptions['defaultValues'],
41+
TSubmitMeta = TOptions['onSubmitMeta'],
42+
>(
43+
defaultOpts: Partial<
44+
FormOptions<
45+
TFormData,
46+
undefined | FormValidateOrFn<TFormData>,
47+
undefined | FormValidateOrFn<TFormData>,
48+
undefined | FormAsyncValidateOrFn<TFormData>,
49+
undefined | FormValidateOrFn<TFormData>,
50+
undefined | FormAsyncValidateOrFn<TFormData>,
51+
undefined | FormValidateOrFn<TFormData>,
52+
undefined | FormAsyncValidateOrFn<TFormData>,
53+
undefined | FormValidateOrFn<TFormData>,
54+
undefined | FormAsyncValidateOrFn<TFormData>,
55+
undefined | FormAsyncValidateOrFn<TFormData>,
56+
TSubmitMeta
57+
>
58+
> &
59+
TOptions,
60+
): TOptions {
861
return defaultOpts
962
}

packages/form-core/tests/FormApi.test-d.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expectTypeOf, it } from 'vitest'
22
import { z } from 'zod'
3-
import { FormApi } from '../src'
3+
import { FormApi, formOptions } from '../src'
44
import type {
55
DeepKeys,
66
GlobalFormValidationError,
@@ -375,3 +375,75 @@ it('should extract the form error type from a global form error', () => {
375375
)[]
376376
>
377377
})
378+
379+
it('listeners should be typed correctly', () => {
380+
type FormData = {
381+
firstName: string
382+
lastName: string
383+
}
384+
385+
const form = new FormApi({
386+
defaultValues: {
387+
firstName: '',
388+
lastName: '',
389+
} as FormData,
390+
listeners: {
391+
onSubmit: ({ formApi }) => {
392+
expectTypeOf(formApi.state.values).toEqualTypeOf<FormData>()
393+
},
394+
},
395+
})
396+
397+
form.handleSubmit()
398+
})
399+
400+
it('listeners should be typed correctly when using formOptions', () => {
401+
type FormData = {
402+
firstName: string
403+
lastName: string
404+
}
405+
406+
const formOpts = formOptions({
407+
defaultValues: {
408+
firstName: 'FirstName',
409+
lastName: 'LastName',
410+
} as FormData,
411+
validators: {
412+
onSubmit: () => {
413+
return {
414+
test: 'test',
415+
}
416+
},
417+
},
418+
listeners: {
419+
onSubmit: ({ formApi }) => {
420+
expectTypeOf(formApi.state.values).toEqualTypeOf<FormData>()
421+
},
422+
},
423+
})
424+
425+
const form = new FormApi({
426+
...formOpts,
427+
listeners: {
428+
// this doesn't error since listeners return void
429+
onSubmit: ({ formApi }) => {
430+
console.log(formApi.state.values)
431+
},
432+
},
433+
validators: {
434+
// this errors becuase the return type is not the same as the validator return type
435+
// onSubmit: () => 'custom on submit',
436+
437+
onSubmit: () => {
438+
return {
439+
test: 'can change the value!',
440+
}
441+
},
442+
onChange: () => {
443+
return 'onChange'
444+
},
445+
},
446+
})
447+
448+
form.handleSubmit()
449+
})

0 commit comments

Comments
 (0)