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/quiet-keys-stay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/form-core': patch
---

Fix: `formOptions.looseSchema` no longer turns optional schema properties into required ones, so the same schema can be passed to `validators[].run`.
17 changes: 15 additions & 2 deletions packages/form-core/src/utils.public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,21 @@ export type InferUnion<TBase, TIncoming> = TBase extends BuiltInType
type InferUnionObject<
in out TBase extends object,
in out TIncoming extends object,
> = {
[K in keyof TBase | keyof TIncoming]: K extends keyof TBase
> = InferUnionProps<
TBase,
TIncoming,
{
[K in keyof TBase as K extends keyof TIncoming ? never : K]: unknown
} & { [K in keyof TIncoming]: unknown }
>

// Mapping over `keyof TKeys` (a type parameter) is homomorphic, so each key
// keeps the `?` modifier it has in `TKeys`. Keys that `TIncoming` declares take
// its modifier, so a schema input that omits a property stays assignable.
// Keys only in `TBase` keep theirs. Mapping over
// `keyof TBase | keyof TIncoming` directly would drop every modifier.
type InferUnionProps<TBase, TIncoming, TKeys> = {
[K in keyof TKeys]: K extends keyof TBase
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Comment on lines +38 to +52

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the type instantiations like with this change? Seems to potentially add a ton of intersection work.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested on the existing form-core type tests, this adds about 0.8% instantiations and check time is unchanged. But this only covers the existing tests with small schemas, so I have not checked the deeply nested ones or the large ones.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll test this out with larger schemas later. Shouldn't change the percentage all that much, especially if it's not copying zod types around.

? K extends keyof TIncoming
? InferUnion<TBase[K], TIncoming[K]>
: TBase[K]
Expand Down
48 changes: 48 additions & 0 deletions packages/form-core/tests/validation.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,54 @@ describe('formOptions', () => {
}>()
})

it('keeps optional schema properties optional with a loose schema', () => {
const schema = z.object({
required: z.string(),
optional: z.string().optional(),
nested: z.object({
optional: z.number().optional(),
}),
})
const options = formOptions.looseSchema(schema, {
defaultValues: {},
validators: [
{
run: schema,
triggers: ['change'],
},
],
})

expectTypeOf(options.defaultValues).toEqualTypeOf<{
required: string | undefined
optional?: string | undefined
nested:
| {
optional?: number | undefined
}
| undefined
}>()
})

it('keeps optional schema properties optional when defaults provide them', () => {
const schema = z.object({
optional: z.string().optional(),
})
const options = formOptions.looseSchema(schema, {
defaultValues: { optional: 'value' },
validators: [
{
run: schema,
triggers: ['change'],
},
],
})

expectTypeOf(options.defaultValues).toEqualTypeOf<{
optional?: string | undefined
}>()
})

it('types callback values when mixed with a loose schema', () => {
type LooseValue = {
name: string
Expand Down