Skip to content

Commit 02476bb

Browse files
fix(react-form): fix createFormHook returning hooks and HoC with any types (#1253)
* fix(form-core): fix typing for formOptions so they don't override additional args passed to form * ci: apply automated fixes and generate docs * fix(react-form): fix createFormHook returning hooks and HoC with any types * chore: add tests * ci: apply automated fixes and generate docs --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent b1edeca commit 02476bb

File tree

5 files changed

+115
-14
lines changed

5 files changed

+115
-14
lines changed

docs/framework/react/reference/functions/createformhook.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ title: createFormHook
1111
function createFormHook<TComponents, TFormComponents>(__namedParameters): object
1212
```
1313

14-
Defined in: [packages/react-form/src/createFormHook.tsx:188](https://github.com/TanStack/form/blob/main/packages/react-form/src/createFormHook.tsx#L188)
14+
Defined in: [packages/react-form/src/createFormHook.tsx:223](https://github.com/TanStack/form/blob/main/packages/react-form/src/createFormHook.tsx#L223)
1515

1616
## Type Parameters
1717

docs/framework/react/reference/functions/createformhookcontexts.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ title: createFormHookContexts
1111
function createFormHookContexts(): object
1212
```
1313

14-
Defined in: [packages/react-form/src/createFormHook.tsx:18](https://github.com/TanStack/form/blob/main/packages/react-form/src/createFormHook.tsx#L18)
14+
Defined in: [packages/react-form/src/createFormHook.tsx:53](https://github.com/TanStack/form/blob/main/packages/react-form/src/createFormHook.tsx#L53)
1515

1616
## Returns
1717

docs/framework/react/reference/interfaces/withformprops.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ title: WithFormProps
77

88
# Interface: WithFormProps\<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnServer, TSubmitMeta, TFieldComponents, TFormComponents, TRenderProps\>
99

10-
Defined in: [packages/react-form/src/createFormHook.tsx:138](https://github.com/TanStack/form/blob/main/packages/react-form/src/createFormHook.tsx#L138)
10+
Defined in: [packages/react-form/src/createFormHook.tsx:173](https://github.com/TanStack/form/blob/main/packages/react-form/src/createFormHook.tsx#L173)
1111

1212
## Extends
1313

@@ -49,7 +49,7 @@ Defined in: [packages/react-form/src/createFormHook.tsx:138](https://github.com/
4949
optional props: TRenderProps;
5050
```
5151

52-
Defined in: [packages/react-form/src/createFormHook.tsx:165](https://github.com/TanStack/form/blob/main/packages/react-form/src/createFormHook.tsx#L165)
52+
Defined in: [packages/react-form/src/createFormHook.tsx:200](https://github.com/TanStack/form/blob/main/packages/react-form/src/createFormHook.tsx#L200)
5353

5454
***
5555

@@ -59,7 +59,7 @@ Defined in: [packages/react-form/src/createFormHook.tsx:165](https://github.com/
5959
render: (props) => Element;
6060
```
6161

62-
Defined in: [packages/react-form/src/createFormHook.tsx:166](https://github.com/TanStack/form/blob/main/packages/react-form/src/createFormHook.tsx#L166)
62+
Defined in: [packages/react-form/src/createFormHook.tsx:201](https://github.com/TanStack/form/blob/main/packages/react-form/src/createFormHook.tsx#L201)
6363

6464
#### Parameters
6565

packages/react-form/src/createFormHook.tsx

+44-9
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,42 @@ import type { ComponentType, Context, JSX, PropsWithChildren } from 'react'
1313
import type { FieldComponent } from './useField'
1414
import type { ReactFormExtendedApi } from './useForm'
1515

16-
type UnwrapOrAny<T> = [T] extends [unknown] ? any : T
16+
/**
17+
* TypeScript inferencing is weird.
18+
*
19+
* If you have:
20+
*
21+
* @example
22+
*
23+
* interface Args<T> {
24+
* arg?: T
25+
* }
26+
*
27+
* function test<T>(arg?: Partial<Args<T>>): T {
28+
* return 0 as any;
29+
* }
30+
*
31+
* const a = test({});
32+
*
33+
* Then `T` will default to `unknown`.
34+
*
35+
* However, if we change `test` to be:
36+
*
37+
* @example
38+
*
39+
* function test<T extends undefined>(arg?: Partial<Args<T>>): T;
40+
*
41+
* Then `T` becomes `undefined`.
42+
*
43+
* Here, we are checking if the passed type `T` extends `DefaultT` and **only**
44+
* `DefaultT`, as if that's the case we assume that inferencing has not occured.
45+
*/
46+
type UnwrapOrAny<T> = [unknown] extends [T] ? any : T
47+
type UnwrapDefaultOrAny<DefaultT, T> = [DefaultT] extends [T]
48+
? [T] extends [DefaultT]
49+
? any
50+
: T
51+
: T
1752

1853
export function createFormHookContexts() {
1954
// We should never hit the `null` case here
@@ -311,14 +346,14 @@ export function createFormHook<
311346
TRenderProps
312347
>): WithFormProps<
313348
UnwrapOrAny<TFormData>,
314-
UnwrapOrAny<TOnMount>,
315-
UnwrapOrAny<TOnChange>,
316-
UnwrapOrAny<TOnChangeAsync>,
317-
UnwrapOrAny<TOnBlur>,
318-
UnwrapOrAny<TOnBlurAsync>,
319-
UnwrapOrAny<TOnSubmit>,
320-
UnwrapOrAny<TOnSubmitAsync>,
321-
UnwrapOrAny<TOnServer>,
349+
UnwrapDefaultOrAny<undefined | FormValidateOrFn<TFormData>, TOnMount>,
350+
UnwrapDefaultOrAny<undefined | FormValidateOrFn<TFormData>, TOnChange>,
351+
UnwrapDefaultOrAny<undefined | FormValidateOrFn<TFormData>, TOnChangeAsync>,
352+
UnwrapDefaultOrAny<undefined | FormValidateOrFn<TFormData>, TOnBlur>,
353+
UnwrapDefaultOrAny<undefined | FormValidateOrFn<TFormData>, TOnBlurAsync>,
354+
UnwrapDefaultOrAny<undefined | FormValidateOrFn<TFormData>, TOnSubmit>,
355+
UnwrapDefaultOrAny<undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync>,
356+
UnwrapDefaultOrAny<undefined | FormValidateOrFn<TFormData>, TOnServer>,
322357
UnwrapOrAny<TSubmitMeta>,
323358
UnwrapOrAny<TComponents>,
324359
UnwrapOrAny<TFormComponents>,

packages/react-form/tests/createFormHook.test-d.tsx

+66
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,70 @@ describe('createFormHook', () => {
182182
},
183183
})
184184
})
185+
186+
it('withForm props should be properly inferred', () => {
187+
const WithFormComponent = withForm({
188+
props: {
189+
prop1: 'test',
190+
prop2: 10,
191+
},
192+
render: ({ form, ...props }) => {
193+
assertType<{
194+
prop1: string
195+
prop2: number
196+
children?: React.ReactNode
197+
}>(props)
198+
return <form.Test />
199+
},
200+
})
201+
})
202+
203+
it("component made from withForm should have it's props properly typed", () => {
204+
const formOpts = formOptions({
205+
defaultValues: {
206+
firstName: 'FirstName',
207+
lastName: 'LastName',
208+
},
209+
})
210+
211+
const appForm = useAppForm(formOpts)
212+
213+
const WithFormComponent = withForm({
214+
...formOpts,
215+
props: {
216+
prop1: 'test',
217+
prop2: 10,
218+
},
219+
render: ({ form, ...props }) => {
220+
assertType<{
221+
prop1: string
222+
prop2: number
223+
}>(props)
224+
return <form.Test />
225+
},
226+
})
227+
228+
const CorrectComponent = (
229+
<WithFormComponent form={appForm} prop1="test" prop2={10} />
230+
)
231+
232+
// @ts-expect-error Missing required props prop1 and prop2
233+
const MissingPropsComponent = <WithFormComponent form={appForm} />
234+
235+
const incorrectFormOpts = formOptions({
236+
defaultValues: {
237+
firstName: 'FirstName',
238+
lastName: 'LastName',
239+
firstNameWrong: 'FirstName',
240+
lastNameWrong: 'LastName',
241+
},
242+
})
243+
244+
const incorrectAppForm = useAppForm(incorrectFormOpts)
245+
246+
const IncorrectFormOptsComponent = (
247+
// @ts-expect-error Incorrect form opts
248+
<WithFormComponent form={incorrectAppForm} prop1="test" prop2={10} />
249+
)
250+
})
185251
})

0 commit comments

Comments
 (0)