sveltekit-superforms
sveltekit-superforms copied to clipboard
FormPathLeaves<> fails when used with a recursive zod schema.
- [x] Before posting an issue, read the FAQ at https://superforms.rocks/faq and search the previous issues.
Description
When used with a recursive zod schema, FormPathLeaves fails with an error such as:
Type of property 'ingredients' circularly references itself in mapped type '{ [K in keyof NonNullable<Ingredient>]-?: K extends string ? NonNullable<NonNullable<Ingredient>[K]> extends object ? `${K}${NonNullable<...> extends unknown[] ? "" : "."}${StringPathLeaves<...> & string}` : never : never; }'
This prevents the use of componentized fields with a form validated with a recursive schema as shown in https://superforms.rocks/components.
If applicable, a MRE https://stackblitz.com/edit/sveltekit-superforms-1-testing-whrq5p?file=src%2Froutes%2Fschema.ts
Observe the error on the reified type ExampleFormPathLeaves. This error occurs wherever FormPathLeaves is used on the form type resulting from validating this schema.
I'm not sure how to handle this, since the whole point of FormPathLeaves is to define the form fields at compile-time, which doesn't work with z.lazy and recursive schemas.
Hm. If possible, could we eliminate the recursive fields and allow use of the form fields for well-defined fields?
For my use case, I am using the non-recursive fields with field components, but this error on the recursive element of the schema prevent its use even on non-recursive elements.
FormPathLeaves doesn't work even without a recursive schema? Can you show me an example?
Ah, I'm saying that if I have a recursive schema, but am using FormPathLeaves to access a non-recursive component, it still fails.
This means that any recursive schema can't use abstracted field components, even if those components are attached to non-recursive elements at the top level.
In my example above: of course children can't be used, but it would be excellent if it were still possible to have 'name' resolve as an allowable value for FormPathLeaves, so an abstract field can be used for the name input.
I noticed that if you let typescript infer the types, it works:
const baseExampleSchema = z.object({
name: z.string()
});
const exampleSchema = baseExampleSchema.extend({
children: z.lazy(() => baseExampleSchema.array().optional())
});
// "name" | `children[${number}].name`
type ExampleFormPathLeaves = FormPathLeaves<z.infer<typeof exampleSchema>>;
@ciscoheat if you let typescript infer the type on the extended zod schema, it works - but that's because the recursive definition is now missing from the zod type! The parse result of exampleSchema now will not be typed properly with the full recursive type. See: https://zod.dev/?id=recursive-types
The effect is the same - you now need two types, one fully typed, one partially typed via inference - and then you use the fully typed type for all zod operations and the partial type for FormPathLeaves. This is a pretty bad solution.
This will be automatically fixed in 2.0, since it won't use the zod schema as a parameter anymore, but the inferred type of it.
@fnimick Do you have time to see if the situation has improved in v2? Can you reproduce the problem in this repo? https://stackblitz.com/edit/superforms-2-testing?file=src%2Froutes%2Fschema.ts
@ciscoheat unfortunately it's still broken for this use case, I think. See: https://stackblitz.com/edit/superforms-2-testing-jswy18
FormPathLeaves which you need to type as an input to an abstracted form component still fails due to the self-referential property, even though that is now coming from the inferred zod type rather than the schema itself.
Yes, it must be handled with a recursive check, which will complicate the StringPathLeaves type even further. If you want to make an attempt at a PR, here it is.
This can be of help: https://dev.to/scooperdev/supporting-circularly-referenced-mapped-types-in-typescript-4825
FormPath(Leaves) has been rewritten in alpha.40, which should make it simpler to check for recursion now.
Stopping on identical types is one way to handle it, another is to count recursion levels with some type trickery.
I'll take a look when I have time. It would be nice if Typescript had a native utility for this... we can dream