sveltekit-superforms
sveltekit-superforms copied to clipboard
accept union type in generic component
Is your feature request related to a problem? Please describe.
To have generic components it's sometimes neccessary to pass down union types where only an intersection of the type is equal.
Demo: https://www.sveltelab.dev/y7gmqoire0bl3u4?files=.%2Fsrc%2Flib%2Funion.svelte
Union component:
export let superForm:
| SuperForm<Infer<typeof eventSchema>>
| SuperForm<Infer<typeof admissionSchema>>
| SuperForm<Infer<typeof eventTemplateSchema>>;
Schemas:
export const eventSchema = z.object({
name: z.string()
});
export const eventTemplateSchema = z.object({
name: z.string(),
id: z.string()
});
export const admissionSchema = z.object({
name: z.string()
});
Generic input component
export let superForm: SuperForm<T> | undefined = undefined;
But TypeScript complains if the schema types differ even slightly from each other.
Type 'SuperForm<{ name: string; }> | SuperForm<{ name: string; }> | SuperForm<{ name: string; id: string; }>' is not assignable to type 'SuperForm<{ name: string; }> | undefined'.
Type 'SuperForm<{ name: string; id: string; }>' is not assignable to type 'SuperForm<{ name: string; }>'.
The types of 'options.onUpdate' are incompatible between these types.
Type '((event: { form: SuperValidated<{ name: string; id: string; }, any, { name: string; id: string; }>; formEl: HTMLFormElement; formElement: HTMLFormElement; cancel: () => void; result: Required<...>; }) => unknown) | undefined' is not assignable to type '((event: { form: SuperValidated<{ name: string; }, any, { name: string; }>; formEl: HTMLFormElement; formElement: HTMLFormElement; cancel: () => void; result: Required<...>; }) => unknown) | undefined'.
Type '(event: { form: SuperValidated<{ name: string; id: string; }, any, { name: string; id: string; }>; formEl: HTMLFormElement; formElement: HTMLFormElement; cancel: () => void; result: Required<...>; }) => unknown' is not assignable to type '(event: { form: SuperValidated<{ name: string; }, any, { name: string; }>; formEl: HTMLFormElement; formElement: HTMLFormElement; cancel: () => void; result: Required<...>; }) => unknown'.
Describe the solution you'd like It would be helpful to make the generics work with unions somehow
Does it work to put the union in the schemas, or in Infer?
SuperForm<Infer<typeof eventSchema | typeof admissionSchema>>
SuperForm<Infer<typeof eventSchema> | Infer<typeof admissionSchema>>
Unfortunately not. I tried all variations 😕
I updated the demo with your suggestion but it still fails
Error: Type 'SuperForm<{ name: string; desc: string | null; }, any>' is not assignable to type 'SuperForm<Infer<ZodObject<{ name: ZodString; id: ZodString; }, "strip", ZodTypeAny, { name: string; id: string; }, { name: string; id: string; }> | ZodObject<...>>>'.
Types of property 'options' are incompatible.
Type 'Partial<{ id: string; applyAction: boolean; invalidateAll: boolean | "force"; resetForm: boolean | (() => boolean); scrollToError: boolean | ScrollIntoViewOptions | "auto" | "smooth" | "off"; ... 24 more ...; legacy: boolean; }>' is not assignable to type 'Partial<{ id: string; applyAction: boolean; invalidateAll: boolean | "force"; resetForm: boolean | (() => boolean); scrollToError: boolean | ScrollIntoViewOptions | "auto" | "smooth" | "off"; ... 24 more ...; legacy: boolean; }> | Partial<...>'.
Type 'Partial<{ id: string; applyAction: boolean; invalidateAll: boolean | "force"; resetForm: boolean | (() => boolean); scrollToError: boolean | ScrollIntoViewOptions | "auto" | "smooth" | "off"; ... 24 more ...; legacy: boolean; }>' is not assignable to type 'Partial<{ id: string; applyAction: boolean; invalidateAll: boolean | "force"; resetForm: boolean | (() => boolean); scrollToError: boolean | ScrollIntoViewOptions | "auto" | "smooth" | "off"; ... 24 more ...; legacy: boolean; }>'. Two different types with this name exist, but they are unrelated.
Types of property 'onUpdate' are incompatible.
Type '((event: { form: SuperValidated<{ name: string; desc: string | null; }, any, { name: string; desc: string | null; }>; formEl: HTMLFormElement; formElement: HTMLFormElement; cancel: () => void; result: Required<...>; }) => unknown) | undefined' is not assignable to type '((event: { form: SuperValidated<{ name: string; }, any, { name: string; }>; formEl: HTMLFormElement; formElement: HTMLFormElement; cancel: () => void; result: Required<...>; }) => unknown) | undefined'.
Type '(event: { form: SuperValidated<{ name: string; desc: string | null; }, any, { name: string; desc: string | null; }>; formEl: HTMLFormElement; formElement: HTMLFormElement; cancel: () => void; result: Required<...>; }) => unknown' is not assignable to type '(event: { form: SuperValidated<{ name: string; }, any, { name: string; }>; formEl: HTMLFormElement; formElement: HTMLFormElement; cancel: () => void; result: Required<...>; }) => unknown'. (ts)
<Union {superForm} />
I though maybe a way to solve this would be to use generics but it also doesn't work. This time the field prop complains.
(See demo - component name is union2.svelte)
<script lang="ts" generics="T extends { name: string }">
import type { SuperForm } from 'sveltekit-superforms/client';
import Input from './input.svelte';
export let superForm: SuperForm<T>;
$: ({ form } = superForm);
</script>
<Input {superForm} field="name" />
Type '"name"' is not assignable to type 'FormPathLeaves<T>'.
Type '"name"' is not assignable to type 'StringPath<T, { filter: "leaves"; objAppend: never; path: ""; type: any; }>'.
ts
You can use either z.union, or define the type as you did:
export const schema = z.union([eventTemplateSchema, eventSchema, admissionSchema]);
export type UnionSchema =
| Infer<typeof eventSchema>
| Infer<typeof admissionSchema>
| Infer<typeof eventTemplateSchema>;
Then use the superForm type parameter to get the correct type for the schema:
+page.svelte
<script lang="ts">
import Union from './union.svelte';
import { superForm as _superForm } from '$lib/index.js';
import type { UnionSchema } from './schemas.js';
export let data;
const superForm = _superForm<UnionSchema>(data.form);
</script>
<Union {superForm} />
union.svelte
<script lang="ts">
import type { SuperForm } from '$lib/index.js';
import { type UnionSchema } from './schemas.js';
import Input from './input.svelte';
export let superForm: SuperForm<UnionSchema>;
</script>
<Input {superForm} field="name" />
Thank you so much for your help!
The problem with that is that if we have e.g. 3 different pages I don't want to use UnionSchema everywhere but just the eventSchema for example.
I think a great solution would be (if that's somehow possible) if we could use generics or maybe create something like a PartialSchema<> type helper that takes a sub set of the schema as an argument:
page 1:
<script lang="ts">
export let superForm: SuperForm<Infer<typeof eventSchema>>;
</script>
<Union {superForm} />
page 2:
<script lang="ts">
export let superForm: SuperForm<Infer<typeof admissionSchema>>;
</script>
<Union {superForm} />
union.svelte
<script lang="ts">
import type { SuperForm } from 'sveltekit-superforms';
import Input from './input.svelte';
export let superForm: SuperForm<PartialSchema<{ name: string }>>;
</script>
<Input {superForm} field="name" />
Yes, please I also need this. The workaround does not really work in my case (as it complains about properties in both schemas but unrelated to the common fields of interest).
I split my form into smaller parts to reuse them on different pages that are similar but not identical. One small part is responsible for the selection of a range of product numbers (productNoFrom and productNoTo). Now I have schemas filterProductsSchema and exportProductsSchema which both contain those fields and are used on different pages.
I would love to exactly the same as the comment right above this one:
<!-- ProductNumbersRangeSelect.svelte -->
<script lang="ts">
import type { SuperForm } from 'sveltekit-superforms';
export let form: SuperForm<{ productNoFrom: string, productNoTo: string }>;
</script>
<Input {form} name="productNoFrom" />
<Input {form} name="productNoTo" />
And call out to this component from both pages:
<!-- src/routes/+page.svelte -->
<script lang="ts">
import { superForm } from 'sveltekit-superforms';
export let data;
const filterForm = superForm(data.filterForm);
</script>
<ProductNumbersRangeSelect form={filterForm} />
<!-- src/routes/export/+page.svelte -->
<script lang="ts">
import { superForm } from 'sveltekit-superforms';
export let data;
const exportForm = superForm(data.exportForm);
</script>
<ProductNumbersRangeSelect form={exportForm} />