fix: Field components should now infer state.value properly

* chore: refactor TS typings for React

* fix: field should now infer state.value properly in React adapter

* chore: fix Vue package typings

* chore: fix linting

* chore: fix React adapter

* chore: improve performance of TData type in FieldApi

* chore: add back index and parent type

* chore: add Vue TSC dep on Vue example

* chore: fix lint and type test

* chore: update Vite stuff

* chore: add implicit dep for Vue and React examples

* chore: add type test pre-req

* chore: install deps from examples in PR CI

* chore: remove filter from more installation
This commit is contained in:
Corbin Crutchley
2023-09-09 03:40:29 -07:00
committed by GitHub
parent b5a768f182
commit 160f71275f
14 changed files with 421 additions and 512 deletions

View File

@@ -0,0 +1,63 @@
import { assertType } from 'vitest'
import * as React from 'react'
import { useForm } from '../useForm'
it('should type state.value properly', () => {
function Comp() {
const form = useForm({
defaultValues: {
firstName: 'test',
age: 84,
},
} as const)
return (
<form.Provider>
<form.Field
name="firstName"
children={(field) => {
assertType<'test'>(field.state.value)
}}
/>
<form.Field
name="age"
children={(field) => {
assertType<84>(field.state.value)
}}
/>
</form.Provider>
)
}
})
it('should type onChange properly', () => {
function Comp() {
const form = useForm({
defaultValues: {
firstName: 'test',
age: 84,
},
} as const)
return (
<form.Provider>
<form.Field
name="firstName"
onChange={(val) => {
assertType<'test'>(val)
return null
}}
children={(field) => null}
/>
<form.Field
name="age"
onChange={(val) => {
assertType<84>(val)
return null
}}
children={(field) => null}
/>
</form.Provider>
)
}
})

View File

@@ -1,8 +1,9 @@
import type { FieldOptions } from '@tanstack/form-core'
import type { FieldOptions, DeepKeys } from '@tanstack/form-core'
export type UseFieldOptions<TData, TFormData> = FieldOptions<
export type UseFieldOptions<
TData,
TFormData
> & {
TFormData,
TName = unknown extends TFormData ? string : DeepKeys<TFormData>,
> = FieldOptions<TData, TFormData, TName> & {
mode?: 'value' | 'array'
}

View File

@@ -3,17 +3,17 @@ import { useStore } from '@tanstack/react-store'
import type {
DeepKeys,
DeepValue,
FieldOptions,
FieldApiOptions,
Narrow,
} from '@tanstack/form-core'
import { FieldApi, functionalUpdate } from '@tanstack/form-core'
import { FieldApi, type FormApi, functionalUpdate } from '@tanstack/form-core'
import { useFormContext, formContext } from './formContext'
import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect'
import type { UseFieldOptions } from './types'
declare module '@tanstack/form-core' {
// eslint-disable-next-line no-shadow
interface FieldApi<TData, TFormData> {
interface FieldApi<_TData, TFormData, Opts, TData> {
Field: FieldComponent<TData, TFormData>
}
}
@@ -25,13 +25,27 @@ export type UseField<TFormData> = <TField extends DeepKeys<TFormData>>(
>,
) => FieldApi<DeepValue<TFormData, TField>, TFormData>
export function useField<TData, TFormData>(
opts: UseFieldOptions<TData, TFormData>,
): FieldApi<TData, TFormData> {
export function useField<
TData,
TFormData,
TName extends unknown extends TFormData
? string
: DeepKeys<TFormData> = unknown extends TFormData
? string
: DeepKeys<TFormData>,
>(
opts: UseFieldOptions<TData, TFormData, TName>,
): FieldApi<
TData,
TFormData,
Omit<typeof opts, 'onMount'> & {
form: FormApi<TFormData>
}
> {
// Get the form API either manually or from context
const { formApi, parentFieldName } = useFormContext()
const [fieldApi] = useState<FieldApi<TData, TFormData>>(() => {
const [fieldApi] = useState(() => {
const name = (
typeof opts.index === 'number'
? [parentFieldName, opts.index, opts.name]
@@ -40,9 +54,13 @@ export function useField<TData, TFormData>(
.filter((d) => d !== undefined)
.join('.')
const api = new FieldApi({ ...opts, form: formApi, name: name as any })
const api = new FieldApi({
...opts,
form: formApi,
name: name,
} as never)
api.Field = Field as any
api.Field = Field as never
return api
})
@@ -56,70 +74,52 @@ export function useField<TData, TFormData>(
})
useStore(
fieldApi.store as any,
fieldApi.store,
opts.mode === 'array'
? (state: any) => {
return [state.meta, Object.keys(state.value || []).length]
}
: undefined,
)
// Instantiates field meta and removes it when unrendered
useIsomorphicLayoutEffect(() => fieldApi.mount(), [fieldApi])
return fieldApi
return fieldApi as never
}
// export type FieldValue<TFormData, TField> = TFormData extends any[]
// ? TField extends `[${infer TIndex extends number | 'i'}].${infer TRest}`
// ? DeepValue<TFormData[TIndex extends 'i' ? number : TIndex], TRest>
// : TField extends `[${infer TIndex extends number | 'i'}]`
// ? TFormData[TIndex extends 'i' ? number : TIndex]
// : never
// : TField extends `${infer TPrefix}[${infer TIndex extends
// | number
// | 'i'}].${infer TRest}`
// ? DeepValue<
// DeepValue<TFormData, TPrefix>[TIndex extends 'i' ? number : TIndex],
// TRest
// >
// : TField extends `${infer TPrefix}[${infer TIndex extends number | 'i'}]`
// ? DeepValue<TFormData, TPrefix>[TIndex extends 'i' ? number : TIndex]
// : DeepValue<TFormData, TField>
type FieldComponentProps<
TParentData,
TFormData,
TField,
TName extends unknown extends TFormData ? string : DeepKeys<TFormData>,
> = {
children: (
fieldApi: FieldApi<
TField,
TFormData,
FieldApiOptions<TField, TFormData, TName>
>,
) => any
} & (TParentData extends any[]
? {
name?: TName
index: number
}
: {
name: TName
index?: never
}) &
Omit<UseFieldOptions<TField, TFormData, TName>, 'name' | 'index'>
export type FieldValue<TFormData, TField> = TFormData extends any[]
? unknown extends TField
? TFormData[number]
: DeepValue<TFormData[number], TField>
: DeepValue<TFormData, TField>
// type Test1 = FieldValue<{ foo: { bar: string }[] }, 'foo'>
// // ^?
// type Test2 = FieldValue<{ foo: { bar: string }[] }, 'foo[i]'>
// // ^?
// type Test3 = FieldValue<{ foo: { bar: string }[] }, 'foo[2].bar'>
// // ^?
export type FieldComponent<TParentData, TFormData> = <TField>({
export type FieldComponent<TParentData, TFormData> = <
// Type of the field
TField,
// Name of the field
TName extends unknown extends TFormData ? string : DeepKeys<TFormData>,
>({
children,
...fieldOptions
}: {
children: (
fieldApi: FieldApi<FieldValue<TParentData, TField>, TFormData>,
) => any
} & Omit<
UseFieldOptions<FieldValue<TParentData, TField>, TFormData>,
'name' | 'index'
> &
(TParentData extends any[]
? {
name?: TField extends undefined ? TField : DeepKeys<TParentData>
index: number
}
: {
name: TField extends undefined ? TField : DeepKeys<TParentData>
index?: never
})) => any
}: FieldComponentProps<TParentData, TFormData, TField, TName>) => any
export function Field<TData, TFormData>({
children,

View File

@@ -28,10 +28,9 @@ export function useForm<TData>(opts?: FormOptions<TData>): FormApi<TData> {
// @ts-ignore
const api = new FormApi<TData>(opts)
// eslint-disable-next-line react/display-name
api.Provider = (props) => (
<formContext.Provider {...props} value={{ formApi: api }} />
)
api.Provider = function Provider(props) {
return <formContext.Provider {...props} value={{ formApi: api }} />
}
api.Field = Field as any
api.useField = useField as any
api.useStore = (