Feat: [Types] Improves "svelte/element" with directive-free types, and a configurable children
Describe the problem
The module "svelte/elements" provides the definitions of HTML attributes that can be used to declare props to spread in a component that "wrap" a HTML element.
For example for a component Button, using directly the type HTMLButtonAttributes
<script lang="ts">
import type { HTMLButtonAttributes } from 'svelte/elements';
let { children, ...rest } : HTMLButtonAttributes = $props();
</script>
<button {...rest}>
{@render children?.()}
</button>
Or via the equivalent using SvelteHTMLElements
<script lang="ts">
import type { SvelteHTMLElements } from 'svelte/elements';
let { children, ...rest } : SvelteHTMLElements['button'] = $props();
</script>
<button {...rest}>
{@render children?.()}
</button>
But there are 2 flaws :
- This include the definition of
bind:andon:directives, which are therefore proposed by autocompletion - The children is defined with zero parameter, and this cannot be changed easily.
In order to remove the bind:/on: directives, II need to write something like that :
let { children, ...rest } : Omit<HTMLButtonAttributes, `bind:${string}` | `on:${string}`> = $props();
And if I need to specify a parameter for the children snippet, I have to write :
let { children, ...rest } : Omit<HTMLButtonAttributes, `bind:${string}` | `on:${string}` | 'children'>
& { children?: Snippet<[number]> } = $props();
Describe the proposed solution
It would be nice if Svelte 5 had an official type to handle this in "svelte/elements".
Something like this might work :
export type SvelteHTMLProps<TagName extends string, Parameters extends unknown[] = []> = {
children?: import('svelte').Snippet<Parameters>;
} & Omit<SvelteHTMLElements[TagName], `bind:${string}` | `on:${string}` | `children`>;
So we can use SvelteHTMLProps<'button'>in order to define our Button component :
<script lang="ts">
import type { SvelteHTMLProps } from 'svelte/elements';
let { children, ...rest } : SvelteHTMLProps<'button'> = $props();
</script>
<button {...rest}>
{@render children?.()}
</button>
Or SvelteHTMLProps<'button', [number]>to define the children parameter type :
<script lang="ts">
import type { SvelteHTMLProps } from 'svelte/elements';
let { children, onclick, ...rest } : SvelteHTMLProps<'button', [number]> = $props();
let count = $state(0);
function countClick(evt) {
count++;
onclick?.(evt);
}
</script>
<button onclick={countClick} {...rest}>
{@render children?.(count)}
</button>
Importance
nice to have
I wonder: there's a disadvantage in directly define those types this way? Obviously with a type argument to specify the children snippets arguments.
There's a related issue somewhere asking for snippet type being relaxed. That would solve most of these problems already. on: and bind: being present... Not sure what best to do here.
As shown it's somewhat straightforward to implement a helper type in user land, so let's wait for more use cases / upvotes first
Hello! My 2 cents is that children should not exist in the base types. Sometimes we write components that allow no children. I think the component should be explicit about the existence children by specifying it in its Props type.
I think the default no-arg children should be the default value, since this is probably the most common case. But we can improve the types to make it easier to remove :
export type SvelteHTMLProps<TagName extends string, Children extends Snippet<unknown[]> | void = Snippet> =
Omit<SvelteHTMLElements[TagName], `bind:${string}` | `on:${string}` | `children`>
& ( Children extends Snippet<unknown[]> ? { children?: Children } : {});
Example :
// extends <button>
let { children, ...rest } : SvelteHTMLProps<'button'> = $props();
// extends <fieldset>, and a children with one parameter of type string :
let { children, ...rest } : SvelteHTMLProps<'fieldset', Snippet<[string]>> = $props();
// extends <img>, without children :
let { ...rest } : SvelteHTMLProps<'img', void> = $props();