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
5 changes: 5 additions & 0 deletions .changeset/tidy-errors-narrow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/form-core': patch
---

Remove the spurious `undefined` from the element type of `field.state.meta.errors`. Each unused validator slot previously contributed `undefined` to the union, so a single form-level Standard Schema produced `(StandardSchemaV1Issue | undefined)[]` and the array could not be iterated without a guard or a cast. The array is already filtered at runtime, and the form-level `errors` type is already wrapped in `NonNullable`; this aligns the field-level type with both.
70 changes: 40 additions & 30 deletions packages/form-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,37 +658,47 @@ export type FieldLikeMetaDerived<
* An array of errors related to the field value.
*/
errors: Array<
| UnwrapOneLevelOfArray<
UnwrapFieldValidateOrFn<TName, TOnMount, TFormOnMount>
>
| UnwrapOneLevelOfArray<
UnwrapFieldValidateOrFn<TName, TOnChange, TFormOnChange>
>
| UnwrapOneLevelOfArray<
UnwrapFieldAsyncValidateOrFn<TName, TOnChangeAsync, TFormOnChangeAsync>
>
| UnwrapOneLevelOfArray<
UnwrapFieldValidateOrFn<TName, TOnBlur, TFormOnBlur>
>
| UnwrapOneLevelOfArray<
UnwrapFieldAsyncValidateOrFn<TName, TOnBlurAsync, TFormOnBlurAsync>
>
| UnwrapOneLevelOfArray<
UnwrapFieldValidateOrFn<TName, TOnSubmit, TFormOnSubmit>
>
| UnwrapOneLevelOfArray<
UnwrapFieldAsyncValidateOrFn<TName, TOnSubmitAsync, TFormOnSubmitAsync>
>
| UnwrapOneLevelOfArray<
UnwrapFieldValidateOrFn<TName, TOnDynamic, TFormOnDynamic>
>
| UnwrapOneLevelOfArray<
UnwrapFieldAsyncValidateOrFn<
TName,
TOnDynamicAsync,
TFormOnDynamicAsync
NonNullable<
| UnwrapOneLevelOfArray<
UnwrapFieldValidateOrFn<TName, TOnMount, TFormOnMount>
>
>
| UnwrapOneLevelOfArray<
UnwrapFieldValidateOrFn<TName, TOnChange, TFormOnChange>
>
| UnwrapOneLevelOfArray<
UnwrapFieldAsyncValidateOrFn<
TName,
TOnChangeAsync,
TFormOnChangeAsync
>
>
| UnwrapOneLevelOfArray<
UnwrapFieldValidateOrFn<TName, TOnBlur, TFormOnBlur>
>
| UnwrapOneLevelOfArray<
UnwrapFieldAsyncValidateOrFn<TName, TOnBlurAsync, TFormOnBlurAsync>
>
| UnwrapOneLevelOfArray<
UnwrapFieldValidateOrFn<TName, TOnSubmit, TFormOnSubmit>
>
| UnwrapOneLevelOfArray<
UnwrapFieldAsyncValidateOrFn<
TName,
TOnSubmitAsync,
TFormOnSubmitAsync
>
>
| UnwrapOneLevelOfArray<
UnwrapFieldValidateOrFn<TName, TOnDynamic, TFormOnDynamic>
>
| UnwrapOneLevelOfArray<
UnwrapFieldAsyncValidateOrFn<
TName,
TOnDynamicAsync,
TFormOnDynamicAsync
>
>
>
>
/**
* A flag that is `true` if the field's value has not been modified by the user. Opposite of `isDirty`.
Expand Down
16 changes: 4 additions & 12 deletions packages/form-core/tests/FieldApi.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,7 @@ it('should have the correct types returned from field validators in array', () =
},
})

expectTypeOf(field.state.meta.errors).toEqualTypeOf<
Array<'123' | undefined>
>()
expectTypeOf(field.state.meta.errors).toEqualTypeOf<Array<'123'>>()
})

it('should have the correct types returned from form validators in array', () => {
Expand Down Expand Up @@ -288,9 +286,7 @@ it('should handle "fields" return types added to the field\'s error array itself
name: 'firstName',
})

expectTypeOf(field.getMeta().errors).toEqualTypeOf<
Array<'Testing' | undefined>
>()
expectTypeOf(field.getMeta().errors).toEqualTypeOf<Array<'Testing'>>()
})

it('should handle "fields" async return types added to the field\'s errorMap itself', () => {
Expand Down Expand Up @@ -340,9 +336,7 @@ it('should handle "fields" async return types added to the field\'s error array
name: 'firstName',
})

expectTypeOf(field.getMeta().errors).toEqualTypeOf<
Array<'Testing' | undefined>
>()
expectTypeOf(field.getMeta().errors).toEqualTypeOf<Array<'Testing'>>()
})

it('should handle "sub-fields" async return types added to the field\'s error array itself', () => {
Expand All @@ -368,9 +362,7 @@ it('should handle "sub-fields" async return types added to the field\'s error ar
name: 'person.firstName',
})

expectTypeOf(field.getMeta().errors).toEqualTypeOf<
Array<'Testing' | undefined>
>()
expectTypeOf(field.getMeta().errors).toEqualTypeOf<Array<'Testing'>>()
})

it('should only have field-level error types returned from parseValueWithSchema and parseValueWithSchemaAsync', () => {
Expand Down
4 changes: 1 addition & 3 deletions packages/form-core/tests/FormGroupApi.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,7 @@ it('should type errors array from group validators', () => {
},
})

expectTypeOf(group.state.meta.errors).toEqualTypeOf<
Array<'change-error' | undefined>
>()
expectTypeOf(group.state.meta.errors).toEqualTypeOf<Array<'change-error'>>()
})

it('should type handleSubmit return as Promise<void>', () => {
Expand Down
54 changes: 54 additions & 0 deletions packages/form-core/tests/standardSchemaValidator.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,58 @@ describe('standard schema validator', () => {
Promise<StandardSchemaV1Issue[] | undefined>
>()
})

it("Should not add `undefined` to the field's error array when only a form-level schema is present", () => {
const form = new FormApi({
defaultValues: {
firstName: '',
},
validators: {
onChange: z.object({
firstName: z.string().min(1, 'Testing'),
}),
},
})

const field = new FieldApi({
form,
name: 'firstName',
})

// The unused validator slots must not leak `undefined` into the element
// union; the errors array is filtered before it reaches field meta.
expectTypeOf(field.getMeta().errors).toEqualTypeOf<
Array<StandardSchemaV1Issue>
>()

// Consequently the issues are iterable without a guard or a cast.
expectTypeOf(
field.getMeta().errors.map((issue) => issue.message),
).toEqualTypeOf<Array<string>>()
})

it("Should keep `undefined` out of the error array while preserving a field validator's own return type", () => {
const form = new FormApi({
defaultValues: {
firstName: '',
},
validators: {
onDynamic: z.object({
firstName: z.string().min(1, 'Testing'),
}),
},
})

const field = new FieldApi({
form,
name: 'firstName',
validators: {
onDynamic: ({ value }) => (value ? undefined : ('Required' as const)),
},
})

expectTypeOf(field.getMeta().errors).toEqualTypeOf<
Array<StandardSchemaV1Issue | 'Required'>
>()
})
})
2 changes: 1 addition & 1 deletion packages/preact-form/tests/useField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,7 @@ describe('useField', () => {
/>
</label>
{field.state.meta.errors.map((err) => {
return <div key={err?.toString()}>{err}</div>
return <div key={err.toString()}>{err}</div>
})}
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion packages/preact-form/tests/useFormGroup.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ describe('useFormGroup field-like meta surface', () => {
})

expectTypeOf(group.state.meta.errors).toEqualTypeOf<
Array<'change-error' | undefined>
Array<'change-error'>
>()
}
})
Expand Down
2 changes: 1 addition & 1 deletion packages/react-form/tests/useField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1093,7 +1093,7 @@ describe('useField', () => {
/>
</label>
{field.state.meta.errors.map((err) => {
return <div key={err?.toString()}>{err}</div>
return <div key={err.toString()}>{err}</div>
})}
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion packages/react-form/tests/useFormGroup.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ describe('useFormGroup field-like meta surface', () => {
})

expectTypeOf(group.state.meta.errors).toEqualTypeOf<
Array<'change-error' | undefined>
Array<'change-error'>
>()
}
})
Expand Down
2 changes: 1 addition & 1 deletion packages/solid-form/tests/createFormGroup.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ describe('createFormGroup field-like meta surface', () => {
}))

expectTypeOf(group().state.meta.errors).toEqualTypeOf<
Array<'change-error' | undefined>
Array<'change-error'>
>()
}
})
Expand Down