Skip to content

Commit e82ebbc

Browse files
CodeAndPartyclaude
andcommitted
fix(form-core): keep undefined out of the field error array type
`FieldLikeMetaDerived['errors']` unions `UnwrapOneLevelOfArray<...>` over all nine validator slots. Every slot without a validator resolves to `undefined`, and `UnwrapOneLevelOfArray<undefined>` is `undefined`, so each unused slot leaked `undefined` into the element type. With a single form-level Standard Schema — eight slots unused — the array inferred as `(StandardSchemaV1Issue | undefined)[]`, which cannot be iterated without a guard or a cast. The value is already filtered at runtime, and the form-level `errors` types already wrap their union in `NonNullable`. Wrap the field-level union to match. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 57a855b commit e82ebbc

10 files changed

Lines changed: 109 additions & 50 deletions

File tree

.changeset/tidy-errors-narrow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/form-core': patch
3+
---
4+
5+
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.

packages/form-core/src/types.ts

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -658,37 +658,47 @@ export type FieldLikeMetaDerived<
658658
* An array of errors related to the field value.
659659
*/
660660
errors: Array<
661-
| UnwrapOneLevelOfArray<
662-
UnwrapFieldValidateOrFn<TName, TOnMount, TFormOnMount>
663-
>
664-
| UnwrapOneLevelOfArray<
665-
UnwrapFieldValidateOrFn<TName, TOnChange, TFormOnChange>
666-
>
667-
| UnwrapOneLevelOfArray<
668-
UnwrapFieldAsyncValidateOrFn<TName, TOnChangeAsync, TFormOnChangeAsync>
669-
>
670-
| UnwrapOneLevelOfArray<
671-
UnwrapFieldValidateOrFn<TName, TOnBlur, TFormOnBlur>
672-
>
673-
| UnwrapOneLevelOfArray<
674-
UnwrapFieldAsyncValidateOrFn<TName, TOnBlurAsync, TFormOnBlurAsync>
675-
>
676-
| UnwrapOneLevelOfArray<
677-
UnwrapFieldValidateOrFn<TName, TOnSubmit, TFormOnSubmit>
678-
>
679-
| UnwrapOneLevelOfArray<
680-
UnwrapFieldAsyncValidateOrFn<TName, TOnSubmitAsync, TFormOnSubmitAsync>
681-
>
682-
| UnwrapOneLevelOfArray<
683-
UnwrapFieldValidateOrFn<TName, TOnDynamic, TFormOnDynamic>
684-
>
685-
| UnwrapOneLevelOfArray<
686-
UnwrapFieldAsyncValidateOrFn<
687-
TName,
688-
TOnDynamicAsync,
689-
TFormOnDynamicAsync
661+
NonNullable<
662+
| UnwrapOneLevelOfArray<
663+
UnwrapFieldValidateOrFn<TName, TOnMount, TFormOnMount>
690664
>
691-
>
665+
| UnwrapOneLevelOfArray<
666+
UnwrapFieldValidateOrFn<TName, TOnChange, TFormOnChange>
667+
>
668+
| UnwrapOneLevelOfArray<
669+
UnwrapFieldAsyncValidateOrFn<
670+
TName,
671+
TOnChangeAsync,
672+
TFormOnChangeAsync
673+
>
674+
>
675+
| UnwrapOneLevelOfArray<
676+
UnwrapFieldValidateOrFn<TName, TOnBlur, TFormOnBlur>
677+
>
678+
| UnwrapOneLevelOfArray<
679+
UnwrapFieldAsyncValidateOrFn<TName, TOnBlurAsync, TFormOnBlurAsync>
680+
>
681+
| UnwrapOneLevelOfArray<
682+
UnwrapFieldValidateOrFn<TName, TOnSubmit, TFormOnSubmit>
683+
>
684+
| UnwrapOneLevelOfArray<
685+
UnwrapFieldAsyncValidateOrFn<
686+
TName,
687+
TOnSubmitAsync,
688+
TFormOnSubmitAsync
689+
>
690+
>
691+
| UnwrapOneLevelOfArray<
692+
UnwrapFieldValidateOrFn<TName, TOnDynamic, TFormOnDynamic>
693+
>
694+
| UnwrapOneLevelOfArray<
695+
UnwrapFieldAsyncValidateOrFn<
696+
TName,
697+
TOnDynamicAsync,
698+
TFormOnDynamicAsync
699+
>
700+
>
701+
>
692702
>
693703
/**
694704
* A flag that is `true` if the field's value has not been modified by the user. Opposite of `isDirty`.

packages/form-core/tests/FieldApi.test-d.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,7 @@ it('should have the correct types returned from field validators in array', () =
221221
},
222222
})
223223

224-
expectTypeOf(field.state.meta.errors).toEqualTypeOf<
225-
Array<'123' | undefined>
226-
>()
224+
expectTypeOf(field.state.meta.errors).toEqualTypeOf<Array<'123'>>()
227225
})
228226

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

291-
expectTypeOf(field.getMeta().errors).toEqualTypeOf<
292-
Array<'Testing' | undefined>
293-
>()
289+
expectTypeOf(field.getMeta().errors).toEqualTypeOf<Array<'Testing'>>()
294290
})
295291

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

343-
expectTypeOf(field.getMeta().errors).toEqualTypeOf<
344-
Array<'Testing' | undefined>
345-
>()
339+
expectTypeOf(field.getMeta().errors).toEqualTypeOf<Array<'Testing'>>()
346340
})
347341

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

371-
expectTypeOf(field.getMeta().errors).toEqualTypeOf<
372-
Array<'Testing' | undefined>
373-
>()
365+
expectTypeOf(field.getMeta().errors).toEqualTypeOf<Array<'Testing'>>()
374366
})
375367

376368
it('should only have field-level error types returned from parseValueWithSchema and parseValueWithSchemaAsync', () => {

packages/form-core/tests/FormGroupApi.test-d.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,7 @@ it('should type errors array from group validators', () => {
116116
},
117117
})
118118

119-
expectTypeOf(group.state.meta.errors).toEqualTypeOf<
120-
Array<'change-error' | undefined>
121-
>()
119+
expectTypeOf(group.state.meta.errors).toEqualTypeOf<Array<'change-error'>>()
122120
})
123121

124122
it('should type handleSubmit return as Promise<void>', () => {

packages/form-core/tests/standardSchemaValidator.test-d.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,58 @@ describe('standard schema validator', () => {
119119
Promise<StandardSchemaV1Issue[] | undefined>
120120
>()
121121
})
122+
123+
it("Should not add `undefined` to the field's error array when only a form-level schema is present", () => {
124+
const form = new FormApi({
125+
defaultValues: {
126+
firstName: '',
127+
},
128+
validators: {
129+
onChange: z.object({
130+
firstName: z.string().min(1, 'Testing'),
131+
}),
132+
},
133+
})
134+
135+
const field = new FieldApi({
136+
form,
137+
name: 'firstName',
138+
})
139+
140+
// The unused validator slots must not leak `undefined` into the element
141+
// union; the errors array is filtered before it reaches field meta.
142+
expectTypeOf(field.getMeta().errors).toEqualTypeOf<
143+
Array<StandardSchemaV1Issue>
144+
>()
145+
146+
// Consequently the issues are iterable without a guard or a cast.
147+
expectTypeOf(
148+
field.getMeta().errors.map((issue) => issue.message),
149+
).toEqualTypeOf<Array<string>>()
150+
})
151+
152+
it("Should keep `undefined` out of the error array while preserving a field validator's own return type", () => {
153+
const form = new FormApi({
154+
defaultValues: {
155+
firstName: '',
156+
},
157+
validators: {
158+
onDynamic: z.object({
159+
firstName: z.string().min(1, 'Testing'),
160+
}),
161+
},
162+
})
163+
164+
const field = new FieldApi({
165+
form,
166+
name: 'firstName',
167+
validators: {
168+
onDynamic: ({ value }) => (value ? undefined : ('Required' as const)),
169+
},
170+
})
171+
172+
expectTypeOf(field.getMeta().errors).toEqualTypeOf<
173+
Array<StandardSchemaV1Issue | 'Required'>
174+
>()
175+
})
122176
})

packages/preact-form/tests/useField.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -747,7 +747,7 @@ describe('useField', () => {
747747
/>
748748
</label>
749749
{field.state.meta.errors.map((err) => {
750-
return <div key={err?.toString()}>{err}</div>
750+
return <div key={err.toString()}>{err}</div>
751751
})}
752752
</div>
753753
)

packages/preact-form/tests/useFormGroup.test-d.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ describe('useFormGroup field-like meta surface', () => {
301301
})
302302

303303
expectTypeOf(group.state.meta.errors).toEqualTypeOf<
304-
Array<'change-error' | undefined>
304+
Array<'change-error'>
305305
>()
306306
}
307307
})

packages/react-form/tests/useField.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1093,7 +1093,7 @@ describe('useField', () => {
10931093
/>
10941094
</label>
10951095
{field.state.meta.errors.map((err) => {
1096-
return <div key={err?.toString()}>{err}</div>
1096+
return <div key={err.toString()}>{err}</div>
10971097
})}
10981098
</div>
10991099
)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ describe('useFormGroup field-like meta surface', () => {
301301
})
302302

303303
expectTypeOf(group.state.meta.errors).toEqualTypeOf<
304-
Array<'change-error' | undefined>
304+
Array<'change-error'>
305305
>()
306306
}
307307
})

packages/solid-form/tests/createFormGroup.test-d.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ describe('createFormGroup field-like meta surface', () => {
301301
}))
302302

303303
expectTypeOf(group().state.meta.errors).toEqualTypeOf<
304-
Array<'change-error' | undefined>
304+
Array<'change-error'>
305305
>()
306306
}
307307
})

0 commit comments

Comments
 (0)