Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions packages/form-core/src/util-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,60 @@ export type DeepKeysOfType<TData, TValue> = Extract<
AnyDeepKeyAndValue<string, TValue>
>['key']

type IsEqual<TValue, TOther> = [TValue] extends [TOther]
? [TOther] extends [TValue]
? true
: false
: false

/**
* Checks whether the form value can safely back a field group value.
* Extra form fields are allowed, but fields exposed by the field group must match exactly.
*/
type IsFieldGroupCompatible<TFormValue, TFieldGroupValue> = [
TFieldGroupValue,
] extends [ReadonlyArray<infer TFieldGroupItem>]
? [TFormValue] extends [ReadonlyArray<infer TFormItem>]
? IsFieldGroupCompatible<TFormItem, TFieldGroupItem>
: false
: [TFieldGroupValue] extends [object]
? [TFormValue] extends [object]
? false extends {
[K in keyof TFieldGroupValue]-?: K extends keyof TFormValue
? IsFieldGroupCompatible<TFormValue[K], TFieldGroupValue[K]>
: false
}[keyof TFieldGroupValue]
? false
: true
: false
: IsEqual<TFormValue, TFieldGroupValue>

/**
* The keys of an object or array, deeply nested and compatible with a field group value.
*/
export type DeepKeysOfFieldGroupType<TData, TValue> = unknown extends TData
? string
: DeepKeysAndValues<TData> extends infer TDeepKeyAndValue
? TDeepKeyAndValue extends AnyDeepKeyAndValue<infer TKey, infer TDeepValue>
? IsFieldGroupCompatible<NonNullable<TDeepValue>, TValue> extends true
? TKey
: never
: never
: never

/**
* The keys of an object or array, deeply nested and compatible with a mapped field group value.
*/
export type DeepKeysOfFieldGroupFieldType<TData, TValue> = unknown extends TData
? string
: DeepKeysAndValues<TData> extends infer TDeepKeyAndValue
? TDeepKeyAndValue extends AnyDeepKeyAndValue<infer TKey, infer TDeepValue>
? IsFieldGroupCompatible<TDeepValue, TValue> extends true
? TKey
: never
: never
: never

/**
* Maps the deep keys of TFormData to the shallow keys of TFieldGroupData.
* Since using template strings as keys is impractical, it relies on shallow keys only.
Expand All @@ -211,3 +265,19 @@ export type FieldsMap<TFormData, TFieldGroupData> =
TFieldGroupData[K]
>
}

/**
* Maps the deep keys of TFormData to the shallow keys of TFieldGroupData with compatible field group value types.
* Since using template strings as keys is impractical, it relies on shallow keys only.
*/
export type FieldGroupFieldsMap<TFormData, TFieldGroupData> =
TFieldGroupData extends any[]
? never
: string extends keyof TFieldGroupData
? never
: {
[K in keyof TFieldGroupData]: DeepKeysOfFieldGroupFieldType<
TFormData,
TFieldGroupData[K]
>
}
7 changes: 4 additions & 3 deletions packages/preact-form/src/createFormHook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import type {
AnyFieldApi,
AnyFormApi,
BaseFormOptions,
DeepKeysOfType,
DeepKeysOfFieldGroupType,
FieldApi,
FieldGroupFieldsMap,
FieldsMap,
FormAsyncValidateOrFn,
FormOptions,
Expand Down Expand Up @@ -468,8 +469,8 @@ export function createFormHook<
>): <
TFormData,
TFields extends
| DeepKeysOfType<TFormData, TFieldGroupData | null | undefined>
| FieldsMap<TFormData, TFieldGroupData>,
| DeepKeysOfFieldGroupType<TFormData, TFieldGroupData>
| FieldGroupFieldsMap<TFormData, TFieldGroupData>,
TOnMount extends undefined | FormValidateOrFn<TFormData>,
TOnChange extends undefined | FormValidateOrFn<TFormData>,
TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
Expand Down
46 changes: 46 additions & 0 deletions packages/preact-form/tests/createFormHook.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,52 @@ describe('createFormHook', () => {
const Component5 = <FieldGroup form={form} fields="nope2" />
})

it('should require exact field value types for withFieldGroup fields', () => {
type PasswordFields = {
password: string | null
}

const FieldGroupPasswordFields = withFieldGroup({
defaultValues: { password: '' } as PasswordFields,
render: function Render() {
return <></>
},
})

const form = useAppForm({
defaultValues: {
reusable: { password: '' },
exact: { password: null as string | null },
},
})

const ExactComponent = (
<FieldGroupPasswordFields form={form} fields="exact" />
)
const ExactMappedComponent = (
<FieldGroupPasswordFields
form={form}
fields={{ password: 'exact.password' }}
/>
)
const TooWideComponent = (
<FieldGroupPasswordFields
form={form}
// @ts-expect-error because the field group could write null into a string-only parent field
fields="reusable"
/>
)
const TooWideMappedComponent = (
<FieldGroupPasswordFields
form={form}
fields={{
// @ts-expect-error because the field group could write null into a string-only parent field
password: 'reusable.password',
}}
/>
)
})

it('should allow interfaces without index signatures to be assigned to `props` in withForm and withFormGroup', () => {
interface TestNoSignature {
title: string
Expand Down
7 changes: 4 additions & 3 deletions packages/react-form/src/createFormHook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import type {
AnyFieldApi,
AnyFormApi,
BaseFormOptions,
DeepKeysOfType,
DeepKeysOfFieldGroupType,
FieldApi,
FieldGroupFieldsMap,
FieldsMap,
FormAsyncValidateOrFn,
FormOptions,
Expand Down Expand Up @@ -469,8 +470,8 @@ export function createFormHook<
>): <
TFormData,
TFields extends
| DeepKeysOfType<TFormData, TFieldGroupData | null | undefined>
| FieldsMap<TFormData, TFieldGroupData>,
| DeepKeysOfFieldGroupType<TFormData, TFieldGroupData>
| FieldGroupFieldsMap<TFormData, TFieldGroupData>,
TOnMount extends undefined | FormValidateOrFn<TFormData>,
TOnChange extends undefined | FormValidateOrFn<TFormData>,
TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
Expand Down
46 changes: 46 additions & 0 deletions packages/react-form/tests/createFormHook.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,52 @@ describe('createFormHook', () => {
const Component5 = <FieldGroup form={form} fields="nope2" />
})

it('should require exact field value types for withFieldGroup fields', () => {
type PasswordFields = {
password: string | null
}

const FieldGroupPasswordFields = withFieldGroup({
defaultValues: { password: '' } as PasswordFields,
render: function Render() {
return <></>
},
})

const form = useAppForm({
defaultValues: {
reusable: { password: '' },
exact: { password: null as string | null },
},
})

const ExactComponent = (
<FieldGroupPasswordFields form={form} fields="exact" />
)
const ExactMappedComponent = (
<FieldGroupPasswordFields
form={form}
fields={{ password: 'exact.password' }}
/>
)
const TooWideComponent = (
<FieldGroupPasswordFields
form={form}
// @ts-expect-error because the field group could write null into a string-only parent field
fields="reusable"
/>
)
const TooWideMappedComponent = (
<FieldGroupPasswordFields
form={form}
fields={{
// @ts-expect-error because the field group could write null into a string-only parent field
password: 'reusable.password',
}}
/>
)
})

it('should allow interfaces without index signatures to be assigned to `props` in withForm and withFormGroup', () => {
interface TestNoSignature {
title: string
Expand Down
7 changes: 4 additions & 3 deletions packages/solid-form/src/createFormHook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import type {
AnyFieldApi,
AnyFormApi,
BaseFormOptions,
DeepKeysOfType,
DeepKeysOfFieldGroupType,
FieldApi,
FieldGroupFieldsMap,
FieldsMap,
FormAsyncValidateOrFn,
FormOptions,
Expand Down Expand Up @@ -493,8 +494,8 @@ export function createFormHook<
>): <
TFormData,
TFields extends
| DeepKeysOfType<TFormData, TFieldGroupData | null | undefined>
| FieldsMap<TFormData, TFieldGroupData>,
| DeepKeysOfFieldGroupType<TFormData, TFieldGroupData>
| FieldGroupFieldsMap<TFormData, TFieldGroupData>,
TOnMount extends undefined | FormValidateOrFn<TFormData>,
TOnChange extends undefined | FormValidateOrFn<TFormData>,
TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
Expand Down