diff --git a/.changeset/silver-arrays-collect.md b/.changeset/silver-arrays-collect.md new file mode 100644 index 0000000000..1041cf0617 --- /dev/null +++ b/.changeset/silver-arrays-collect.md @@ -0,0 +1,5 @@ +--- +'@tanstack/form-core': patch +--- + +Allow optional array fields to use the form array helper methods. diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 80770f8897..eb37fb03d0 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -31,7 +31,12 @@ import type { ValidationErrorMap, } from './types' import type { ReadonlyStore } from '@tanstack/store' -import type { DeepKeys, DeepValue, RejectPromiseValidator } from './util-types' +import type { + ArrayElement, + DeepKeys, + DeepValue, + RejectPromiseValidator, +} from './util-types' import type { StandardSchemaV1, TStandardSchemaValidatorValue, @@ -1102,12 +1107,9 @@ export class FieldApi< /** * Pushes a new value to the field. */ - pushValue = ( - value: TData extends any[] ? TData[number] : never, - options?: UpdateMetaOptions, - ) => { + pushValue = (value: ArrayElement, options?: UpdateMetaOptions) => { this.form.pushFieldValue( - this.name, + this.name as never, value as any, mergeOpts(options, { dontRunListeners: true }), ) @@ -1122,11 +1124,11 @@ export class FieldApi< */ insertValue = ( index: number, - value: TData extends any[] ? TData[number] : never, + value: ArrayElement, options?: UpdateMetaOptions, ) => { this.form.insertFieldValue( - this.name, + this.name as never, index, value as any, mergeOpts(options, { dontRunListeners: true }), @@ -1142,11 +1144,11 @@ export class FieldApi< */ replaceValue = ( index: number, - value: TData extends any[] ? TData[number] : never, + value: ArrayElement, options?: UpdateMetaOptions, ) => { this.form.replaceFieldValue( - this.name, + this.name as never, index, value as any, mergeOpts(options, { dontRunListeners: true }), @@ -1162,7 +1164,7 @@ export class FieldApi< */ removeValue = (index: number, options?: UpdateMetaOptions) => { this.form.removeFieldValue( - this.name, + this.name as never, index, mergeOpts(options, { dontRunListeners: true }), ) @@ -1181,7 +1183,7 @@ export class FieldApi< options?: UpdateMetaOptions, ) => { this.form.swapFieldValues( - this.name, + this.name as never, aIndex, bIndex, mergeOpts(options, { dontRunListeners: true }), @@ -1197,7 +1199,7 @@ export class FieldApi< */ moveValue = (aIndex: number, bIndex: number, options?: UpdateMetaOptions) => { this.form.moveFieldValues( - this.name, + this.name as never, aIndex, bIndex, mergeOpts(options, { dontRunListeners: true }), @@ -1213,7 +1215,7 @@ export class FieldApi< */ clearValues = (options?: UpdateMetaOptions) => { this.form.clearFieldValues( - this.name, + this.name as never, mergeOpts(options, { dontRunListeners: true }), ) diff --git a/packages/form-core/src/FieldGroupApi.ts b/packages/form-core/src/FieldGroupApi.ts index a7fac99519..4a25489b6d 100644 --- a/packages/form-core/src/FieldGroupApi.ts +++ b/packages/form-core/src/FieldGroupApi.ts @@ -15,7 +15,9 @@ import type { } from './FormApi' import type { FieldOptions } from './FieldApi' import type { + ArrayElement, DeepKeys, + DeepKeysOfArray, DeepKeysOfType, DeepValue, FieldsMap, @@ -312,14 +314,14 @@ export class FieldGroupApi< * Validates the children of a specified array in the form starting from a given index until the end using the correct handlers for a given validation type. */ validateArrayFieldsStartingFrom = async < - TField extends DeepKeysOfType, + TField extends DeepKeysOfArray, >( field: TField, index: number, cause: ValidationCause, ) => { return this.form.validateArrayFieldsStartingFrom( - this.getFormFieldName(field), + this.getFormFieldName(field) as never, index, cause, ) @@ -399,15 +401,13 @@ export class FieldGroupApi< /** * Pushes a value into an array field. */ - pushFieldValue = >( + pushFieldValue = >( field: TField, - value: DeepValue extends any[] - ? DeepValue[number] - : never, + value: ArrayElement>, opts?: UpdateMetaOptions, ) => { return this.form.pushFieldValue( - this.getFormFieldName(field), + this.getFormFieldName(field) as never, // since unknown doesn't extend an array, it types `value` as never. value as never, opts, @@ -417,18 +417,14 @@ export class FieldGroupApi< /** * Insert a value into an array field at the specified index. */ - insertFieldValue = async < - TField extends DeepKeysOfType, - >( + insertFieldValue = async >( field: TField, index: number, - value: DeepValue extends any[] - ? DeepValue[number] - : never, + value: ArrayElement>, opts?: UpdateMetaOptions, ) => { return this.form.insertFieldValue( - this.getFormFieldName(field), + this.getFormFieldName(field) as never, index, // since unknown doesn't extend an array, it types `value` as never. value as never, @@ -439,18 +435,14 @@ export class FieldGroupApi< /** * Replaces a value into an array field at the specified index. */ - replaceFieldValue = async < - TField extends DeepKeysOfType, - >( + replaceFieldValue = async >( field: TField, index: number, - value: DeepValue extends any[] - ? DeepValue[number] - : never, + value: ArrayElement>, opts?: UpdateMetaOptions, ) => { return this.form.replaceFieldValue( - this.getFormFieldName(field), + this.getFormFieldName(field) as never, index, // since unknown doesn't extend an array, it types `value` as never. value as never, @@ -461,27 +453,29 @@ export class FieldGroupApi< /** * Removes a value from an array field at the specified index. */ - removeFieldValue = async < - TField extends DeepKeysOfType, - >( + removeFieldValue = async >( field: TField, index: number, opts?: UpdateMetaOptions, ) => { - return this.form.removeFieldValue(this.getFormFieldName(field), index, opts) + return this.form.removeFieldValue( + this.getFormFieldName(field) as never, + index, + opts, + ) } /** * Swaps the values at the specified indices within an array field. */ - swapFieldValues = >( + swapFieldValues = >( field: TField, index1: number, index2: number, opts?: UpdateMetaOptions, ) => { return this.form.swapFieldValues( - this.getFormFieldName(field), + this.getFormFieldName(field) as never, index1, index2, opts, @@ -491,25 +485,28 @@ export class FieldGroupApi< /** * Moves the value at the first specified index to the second specified index within an array field. */ - moveFieldValues = >( + moveFieldValues = >( field: TField, index1: number, index2: number, opts?: UpdateMetaOptions, ) => { return this.form.moveFieldValues( - this.getFormFieldName(field), + this.getFormFieldName(field) as never, index1, index2, opts, ) } - clearFieldValues = >( + clearFieldValues = >( field: TField, opts?: UpdateMetaOptions, ) => { - return this.form.clearFieldValues(this.getFormFieldName(field), opts) + return this.form.clearFieldValues( + this.getFormFieldName(field) as never, + opts, + ) } /** diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 4400017d30..a653902e78 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -54,13 +54,24 @@ import type { FormGroupState, } from './FormGroupApi' import type { + ArrayElement, DeepKeys, + DeepKeysOfArray, DeepKeysOfType, DeepValue, RejectPromiseValidator, } from './util-types' import type { Updater } from './utils' +/** + * Returns array field values as arrays so optional or nullable array helpers can operate safely. + */ +function getArrayFieldValue( + value: TValue, +): Array> { + return Array.isArray(value) ? value : [] +} + /** * @private */ @@ -1890,7 +1901,7 @@ export class FormApi< * Validates the children of a specified array in the form starting from a given index until the end using the correct handlers for a given validation type. */ validateArrayFieldsStartingFrom = async < - TField extends DeepKeysOfType, + TField extends DeepKeysOfArray, >( field: TField, index: number, @@ -2712,37 +2723,34 @@ export class FormApi< /** * Pushes a value into an array field. */ - pushFieldValue = >( + pushFieldValue = >( field: TField, - value: DeepValue extends any[] - ? DeepValue[number] - : never, + value: ArrayElement>, options?: UpdateMetaOptions, ) => { this.setFieldValue( field, - (prev) => [...(Array.isArray(prev) ? prev : []), value] as any, + (prev) => [...getArrayFieldValue(prev), value] as any, options, ) metaHelper(this).bumpArrayVersion(field) } - insertFieldValue = async >( + insertFieldValue = async >( field: TField, index: number, - value: DeepValue extends any[] - ? DeepValue[number] - : never, + value: ArrayElement>, options?: UpdateMetaOptions, ) => { this.setFieldValue( field, (prev) => { + const previousValue = getArrayFieldValue(prev) return [ - ...(prev as DeepValue[]).slice(0, index), + ...previousValue.slice(0, index), value, - ...(prev as DeepValue[]).slice(index), + ...previousValue.slice(index), ] as any }, mergeOpts(options, { dontValidate: true }), @@ -2765,18 +2773,16 @@ export class FormApi< /** * Replaces a value into an array field at the specified index. */ - replaceFieldValue = async >( + replaceFieldValue = async >( field: TField, index: number, - value: DeepValue extends any[] - ? DeepValue[number] - : never, + value: ArrayElement>, options?: UpdateMetaOptions, ) => { this.setFieldValue( field, (prev) => { - return (prev as DeepValue[]).map((d, i) => + return getArrayFieldValue(prev).map((d, i) => i === index ? value : d, ) as any }, @@ -2796,7 +2802,7 @@ export class FormApi< /** * Removes a value from an array field at the specified index. */ - removeFieldValue = async >( + removeFieldValue = async >( field: TField, index: number, options?: UpdateMetaOptions, @@ -2810,9 +2816,7 @@ export class FormApi< this.setFieldValue( field, (prev) => { - return (prev as DeepValue[]).filter( - (_d, i) => i !== index, - ) as any + return getArrayFieldValue(prev).filter((_d, i) => i !== index) as any }, mergeOpts(options, { dontValidate: true }), ) @@ -2836,7 +2840,7 @@ export class FormApi< /** * Swaps the values at the specified indices within an array field. */ - swapFieldValues = >( + swapFieldValues = >( field: TField, index1: number, index2: number, @@ -2845,9 +2849,16 @@ export class FormApi< this.setFieldValue( field, (prev: any) => { - const prev1 = prev[index1]! - const prev2 = prev[index2]! - return setBy(setBy(prev, `${index1}`, prev2), `${index2}`, prev1) + const previousValue = getArrayFieldValue(prev) + if (!previousValue.length) return previousValue as any + + const prev1 = previousValue[index1]! + const prev2 = previousValue[index2]! + return setBy( + setBy(previousValue, `${index1}`, prev2), + `${index2}`, + prev1, + ) }, mergeOpts(options, { dontValidate: true }), ) @@ -2868,7 +2879,7 @@ export class FormApi< /** * Moves the value at the first specified index to the second specified index within an array field. */ - moveFieldValues = >( + moveFieldValues = >( field: TField, index1: number, index2: number, @@ -2877,7 +2888,10 @@ export class FormApi< this.setFieldValue( field, (prev: any) => { - const next: any = [...prev] + const previousValue = getArrayFieldValue(prev) + if (!previousValue.length) return previousValue as any + + const next: any = [...previousValue] next.splice(index2, 0, next.splice(index1, 1)[0]) return next }, @@ -2900,7 +2914,7 @@ export class FormApi< /** * Clear all values within an array field. */ - clearFieldValues = >( + clearFieldValues = >( field: TField, options?: UpdateMetaOptions, ) => { diff --git a/packages/form-core/src/FormGroupApi.ts b/packages/form-core/src/FormGroupApi.ts index 4790d5505c..251eb2214e 100644 --- a/packages/form-core/src/FormGroupApi.ts +++ b/packages/form-core/src/FormGroupApi.ts @@ -49,7 +49,9 @@ import type { import type { AsyncValidator, SyncValidator, Updater } from './utils' import type { ReadonlyStore } from '@tanstack/store' import type { + ArrayElement, DeepKeys, + DeepKeysOfArray, DeepKeysOfType, DeepValue, UnwrapOneLevelOfArray, @@ -2184,13 +2186,17 @@ export class FormGroupApi< } validateArrayFieldsStartingFrom = < - TField extends DeepKeysOfType, + TField extends DeepKeysOfArray, >( field: TField, index: number, cause: ValidationCause, ) => { - return this.form.validateArrayFieldsStartingFrom(field, index, cause) + return this.form.validateArrayFieldsStartingFrom( + field as never, + index, + cause, + ) } validateField = >( @@ -2232,49 +2238,49 @@ export class FormGroupApi< return this.form.deleteField(field) } - pushFieldValue = >( + pushFieldValue = >( field: TField, - value: any, + value: ArrayElement>, ) => { - return this.form.pushFieldValue(field, value) + return this.form.pushFieldValue(field as never, value) } - insertFieldValue = >( + insertFieldValue = >( field: TField, index: number, - value: any, + value: ArrayElement>, ) => { - return this.form.insertFieldValue(field, index, value) + return this.form.insertFieldValue(field as never, index, value) } - replaceFieldValue = >( + replaceFieldValue = >( field: TField, index: number, - value: any, + value: ArrayElement>, ) => { - return this.form.replaceFieldValue(field, index, value) + return this.form.replaceFieldValue(field as never, index, value) } - swapFieldValues = >( + swapFieldValues = >( field: TField, index1: number, index2: number, ) => { - return this.form.swapFieldValues(field, index1, index2) + return this.form.swapFieldValues(field as never, index1, index2) } - moveFieldValues = >( + moveFieldValues = >( field: TField, fromIndex: number, toIndex: number, ) => { - return this.form.moveFieldValues(field, fromIndex, toIndex) + return this.form.moveFieldValues(field as never, fromIndex, toIndex) } - clearFieldValues = >( + clearFieldValues = >( field: TField, ) => { - return this.form.clearFieldValues(field) + return this.form.clearFieldValues(field as never) } resetField = >( @@ -2283,11 +2289,11 @@ export class FormGroupApi< return this.form.resetField(field) } - removeFieldValue = >( + removeFieldValue = >( field: TField, index: number, ) => { - return this.form.removeFieldValue(field, index) + return this.form.removeFieldValue(field as never, index) } areRelatedFieldsValid = () => { diff --git a/packages/form-core/src/types.ts b/packages/form-core/src/types.ts index b3c90a8011..24aad223f2 100644 --- a/packages/form-core/src/types.ts +++ b/packages/form-core/src/types.ts @@ -5,7 +5,9 @@ import type { FieldValidateOrFn, } from './FieldApi' import type { + ArrayElement, DeepKeys, + DeepKeysOfArray, DeepKeysOfType, DeepValue, UnwrapOneLevelOfArray, @@ -186,9 +188,7 @@ export interface FormLikeAPI { /** * Validates the children of a specified array in the form starting from a given index until the end using the correct handlers for a given validation type. */ - validateArrayFieldsStartingFrom: < - TField extends DeepKeysOfType, - >( + validateArrayFieldsStartingFrom: >( field: TField, index: number, cause: ValidationCause, @@ -247,42 +247,36 @@ export interface FormLikeAPI { /** * Pushes a value into an array field. */ - pushFieldValue: >( + pushFieldValue: >( field: TField, - value: DeepValue extends any[] - ? DeepValue[number] - : never, + value: ArrayElement>, opts?: UpdateMetaOptions, ) => void /** * Insert a value into an array field at the specified index. */ - insertFieldValue: >( + insertFieldValue: >( field: TField, index: number, - value: DeepValue extends any[] - ? DeepValue[number] - : never, + value: ArrayElement>, opts?: UpdateMetaOptions, ) => Promise /** * Replaces a value into an array field at the specified index. */ - replaceFieldValue: >( + replaceFieldValue: >( field: TField, index: number, - value: DeepValue extends any[] - ? DeepValue[number] - : never, + value: ArrayElement>, opts?: UpdateMetaOptions, ) => Promise /** * Removes a value from an array field at the specified index. */ - removeFieldValue: >( + removeFieldValue: >( field: TField, index: number, opts?: UpdateMetaOptions, @@ -291,7 +285,7 @@ export interface FormLikeAPI { /** * Swaps the values at the specified indices within an array field. */ - swapFieldValues: >( + swapFieldValues: >( field: TField, index1: number, index2: number, @@ -301,7 +295,7 @@ export interface FormLikeAPI { /** * Moves the value at the first specified index to the second specified index within an array field. */ - moveFieldValues: >( + moveFieldValues: >( field: TField, index1: number, index2: number, @@ -311,7 +305,7 @@ export interface FormLikeAPI { /** * Clear all values within an array field. */ - clearFieldValues: >( + clearFieldValues: >( field: TField, opts?: UpdateMetaOptions, ) => void diff --git a/packages/form-core/src/util-types.ts b/packages/form-core/src/util-types.ts index a27da3a92e..bf33cbe62d 100644 --- a/packages/form-core/src/util-types.ts +++ b/packages/form-core/src/util-types.ts @@ -196,6 +196,24 @@ export type DeepKeysOfType = Extract< AnyDeepKeyAndValue >['key'] +/** + * The keys of an object or array, deeply nested and only with a nullable or optional array value. + */ +export type DeepKeysOfArray = + DeepKeysAndValues extends infer TKeyAndValue + ? TKeyAndValue extends AnyDeepKeyAndValue + ? NonNullable extends any[] + ? TKeyAndValue['key'] + : never + : never + : never + +/** + * The item type of a nullable or optional array. + */ +export type ArrayElement = + NonNullable extends (infer TElement)[] ? TElement : 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. diff --git a/packages/form-core/tests/FieldApi.test-d.ts b/packages/form-core/tests/FieldApi.test-d.ts index 01f3e136b6..cf6a03adeb 100644 --- a/packages/form-core/tests/FieldApi.test-d.ts +++ b/packages/form-core/tests/FieldApi.test-d.ts @@ -149,6 +149,56 @@ it('should type an array sub-field properly', () => { expectTypeOf(field.state.value).toEqualTypeOf() }) +it('should allow optional array fields to use array value methods', () => { + type FormValues = { + aliases?: string[] + relatives: { name: string }[] | undefined + tags: string[] | null + title: string | undefined + } + + const form = new FormApi({ + defaultValues: { + title: undefined, + } as FormValues, + }) + + const aliases = new FieldApi({ + form, + name: 'aliases', + }) + const relatives = new FieldApi({ + form, + name: 'relatives', + }) + const tags = new FieldApi({ + form, + name: 'tags', + }) + const title = new FieldApi({ + form, + name: 'title', + }) + + aliases.pushValue('alias') + aliases.insertValue(0, 'alias') + aliases.replaceValue(0, 'alias') + aliases.removeValue(0) + aliases.swapValues(0, 1) + aliases.moveValue(0, 1) + aliases.clearValues() + + relatives.pushValue({ name: 'relative' }) + relatives.insertValue(0, { name: 'relative' }) + relatives.replaceValue(0, { name: 'relative' }) + tags.pushValue('tag') + + // @ts-expect-error non-array fields are still rejected + title.pushValue('title') + // @ts-expect-error array values must still match the array element type + aliases.pushValue(1) +}) + it('should have the correct types returned from form validators', () => { const form = new FormApi({ defaultValues: { diff --git a/packages/form-core/tests/FieldGroupApi.test-d.ts b/packages/form-core/tests/FieldGroupApi.test-d.ts index 1e0ed9e499..a31dbc2b37 100644 --- a/packages/form-core/tests/FieldGroupApi.test-d.ts +++ b/packages/form-core/tests/FieldGroupApi.test-d.ts @@ -168,6 +168,49 @@ describe('fieldGroupApi', () => { }) }) + it('should allow optional array fields for array-specific methods', () => { + type FormValues = { + nested: { + aliases?: string[] + title: string | undefined + } + } + + const form = new FormApi({ + defaultValues: { + nested: { + title: undefined, + }, + } as FormValues, + }) + + const group = new FieldGroupApi({ + form, + defaultValues: { + aliases: undefined, + title: undefined, + } as { + aliases?: string[] + title: string | undefined + }, + fields: 'nested', + }) + + group.pushFieldValue('aliases', 'alias') + group.insertFieldValue('aliases', 0, 'alias') + group.replaceFieldValue('aliases', 0, 'alias') + group.removeFieldValue('aliases', 0) + group.swapFieldValues('aliases', 0, 1) + group.moveFieldValues('aliases', 0, 1) + group.clearFieldValues('aliases') + group.validateArrayFieldsStartingFrom('aliases', 0, 'change') + + // @ts-expect-error non-array fields are still rejected + group.removeFieldValue('title', 0) + // @ts-expect-error array values must still match the array element type + group.pushFieldValue('aliases', 1) + }) + it('should allow null and undefined for fields when string', () => { type FormValues = { foo: diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index c57bb044e8..ce6a357667 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -328,6 +328,67 @@ describe('form api', () => { ]) }) + it('should handle array helpers when optional array field values are missing', async () => { + type FormValues = { + optionalNames?: string[] + nullableNames: string[] | null + } + + const form = new FormApi({ + defaultValues: { + optionalNames: undefined, + nullableNames: null, + } as FormValues, + }) + form.mount() + + await form.insertFieldValue('optionalNames', 0, 'insert', { + dontValidate: true, + }) + expect(form.getFieldValue('optionalNames')).toStrictEqual(['insert']) + + form.setFieldValue('optionalNames', undefined) + await form.replaceFieldValue('optionalNames', 0, 'replace', { + dontValidate: true, + }) + expect(form.getFieldValue('optionalNames')).toStrictEqual([]) + + form.setFieldValue('optionalNames', undefined) + await form.removeFieldValue('optionalNames', 0, { dontValidate: true }) + expect(form.getFieldValue('optionalNames')).toStrictEqual([]) + + form.setFieldValue('optionalNames', undefined) + form.swapFieldValues('optionalNames', 0, 1, { dontValidate: true }) + expect(form.getFieldValue('optionalNames')).toStrictEqual([]) + + form.setFieldValue('optionalNames', undefined) + form.moveFieldValues('optionalNames', 0, 1, { dontValidate: true }) + expect(form.getFieldValue('optionalNames')).toStrictEqual([]) + + await form.insertFieldValue('nullableNames', 0, 'insert', { + dontValidate: true, + }) + expect(form.getFieldValue('nullableNames')).toStrictEqual(['insert']) + + form.setFieldValue('nullableNames', null) + await form.replaceFieldValue('nullableNames', 0, 'replace', { + dontValidate: true, + }) + expect(form.getFieldValue('nullableNames')).toStrictEqual([]) + + form.setFieldValue('nullableNames', null) + await form.removeFieldValue('nullableNames', 0, { dontValidate: true }) + expect(form.getFieldValue('nullableNames')).toStrictEqual([]) + + form.setFieldValue('nullableNames', null) + form.swapFieldValues('nullableNames', 0, 1, { dontValidate: true }) + expect(form.getFieldValue('nullableNames')).toStrictEqual([]) + + form.setFieldValue('nullableNames', null) + form.moveFieldValues('nullableNames', 0, 1, { dontValidate: true }) + expect(form.getFieldValue('nullableNames')).toStrictEqual([]) + }) + it("should run onChange validation when pushing an array field's value", () => { const form = new FormApi({ defaultValues: { diff --git a/packages/form-core/tests/FormApi.test-d.ts b/packages/form-core/tests/FormApi.test-d.ts index 667729b8dc..6b0ca9af8b 100644 --- a/packages/form-core/tests/FormApi.test-d.ts +++ b/packages/form-core/tests/FormApi.test-d.ts @@ -282,6 +282,46 @@ it('should only allow array fields for array-specific methods', () => { const validate3 = form.validateArrayFieldsStartingFrom }) +it('should allow optional array fields for array-specific methods', () => { + type FormValues = { + aliases?: string[] + relatives: { name: string }[] | undefined + tags: string[] | null + title: string | undefined + empty: null + missing: undefined + } + + const form = new FormApi({ + defaultValues: { + title: undefined, + } as FormValues, + }) + + form.pushFieldValue('aliases', 'alias') + form.insertFieldValue('aliases', 0, 'alias') + form.replaceFieldValue('aliases', 0, 'alias') + form.removeFieldValue('aliases', 0) + form.swapFieldValues('aliases', 0, 1) + form.moveFieldValues('aliases', 0, 1) + form.clearFieldValues('aliases') + form.validateArrayFieldsStartingFrom('aliases', 0, 'change') + + form.pushFieldValue('relatives', { name: 'relative' }) + form.insertFieldValue('relatives', 0, { name: 'relative' }) + form.replaceFieldValue('relatives', 0, { name: 'relative' }) + form.pushFieldValue('tags', 'tag') + + // @ts-expect-error non-array fields are still rejected + form.removeFieldValue('title', 0) + // @ts-expect-error null-only fields are still rejected + form.removeFieldValue('empty', 0) + // @ts-expect-error undefined-only fields are still rejected + form.removeFieldValue('missing', 0) + // @ts-expect-error array values must still match the array element type + form.pushFieldValue('aliases', 1) +}) + it('should infer full field name union for form.resetField parameters', () => { type FormData = { shallow: string diff --git a/packages/form-core/tests/FormGroupApi.test-d.ts b/packages/form-core/tests/FormGroupApi.test-d.ts index 7d6871a671..7bac694a07 100644 --- a/packages/form-core/tests/FormGroupApi.test-d.ts +++ b/packages/form-core/tests/FormGroupApi.test-d.ts @@ -279,3 +279,45 @@ it('should type setValue updater properly', () => { return { name: 'updated' } }) }) + +it('should allow optional array fields for array-specific methods', () => { + type FormValues = { + step1: { + aliases?: string[] + relatives: { name: string }[] | undefined + title: string | undefined + } + } + + const form = new FormApi({ + defaultValues: { + step1: { + title: undefined, + }, + } as FormValues, + }) + + const group = new FormGroupApi({ + name: 'step1', + form, + onGroupSubmit: () => {}, + }) + + group.pushFieldValue('step1.aliases', 'alias') + group.insertFieldValue('step1.aliases', 0, 'alias') + group.replaceFieldValue('step1.aliases', 0, 'alias') + group.removeFieldValue('step1.aliases', 0) + group.swapFieldValues('step1.aliases', 0, 1) + group.moveFieldValues('step1.aliases', 0, 1) + group.clearFieldValues('step1.aliases') + group.validateArrayFieldsStartingFrom('step1.aliases', 0, 'change') + + group.pushFieldValue('step1.relatives', { name: 'relative' }) + group.insertFieldValue('step1.relatives', 0, { name: 'relative' }) + group.replaceFieldValue('step1.relatives', 0, { name: 'relative' }) + + // @ts-expect-error non-array fields are still rejected + group.removeFieldValue('step1.title', 0) + // @ts-expect-error array values must still match the array element type + group.pushFieldValue('step1.aliases', 1) +})