sveltejs / svelte

web development for the rest of us
https://svelte.dev
MIT License
80.17k stars 4.27k forks source link

Feat: [Types] Improves "svelte/element" with directive-free types, and a configurable children #13653

Open adiguba opened 1 month ago

adiguba commented 1 month ago

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 :

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

paoloricciuti commented 1 month ago

I wonder: there's a disadvantage in directly define those types this way? Obviously with a type argument to specify the children snippets arguments.

dummdidumm commented 1 month ago

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

webJose commented 1 month ago

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.

adiguba commented 1 month ago

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();