sveltejs / svelte

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

Improve typing on `children` property in `HTMLAttributes` #13598

Open kwangure opened 1 month ago

kwangure commented 1 month ago

Describe the bug

The elements in 'svelte/elements' extend the following type:

export interface DOMAttributes<T extends EventTarget> {
    // Implicit children prop every element has
    // Add this here so that libraries doing `let { ...props }: HTMLButtonAttributes = $props()` don't need a separate interface
    children?: import('svelte').Snippet;
}

The children property on the interface is intended to allow uses case like these:

<script lang='ts'>
    import type { HTMLButtonAttributes } from 'svelte/elements';
    const { children, ...restProps }: HTMLButtonAttributes = $props();
</script>

<button {...restProps}>{@render children?.()}</button>

But inadvertently now prevents uses like these:

<script lang='ts'>
    import type { HTMLButtonAttributes } from 'svelte/elements';
    import type { Snippet } from 'svelte';

    interface Props /* <-- error */ extends HTMLButtonAttributes  {
        children: Snippet<[string]>
    }
    const { children, ...restProps }: Props = $props();
</script>

<button {...restProps}>{@render children?.('foo')}</button>

With the error being:

Interface 'Props' incorrectly extends interface 'HTMLButtonAttributes'.
  Types of property 'children' are incompatible.
    Type 'Snippet<[string]>' is not assignable to type 'Snippet<[]>'.
      Target signature provides too few arguments. Expected 1 or more, but got 0.

It would be nice if this supported both patterns of usage.

That said, it is possible to use in its current state, though not great.

interface Props extends Omit<HTMLButtonAttributes, 'children'>  {
    children: Snippet<[string]>
}

Reproduction

N/A

Logs

No response

System Info

svelte: ^5.0.0-next.264 => 5.0.0-next.264

Severity

annoyance

fcrozatier commented 1 month ago

These patterns are bad anyway because in the end the editor will lie about all bindings, on: directives etc.

Capture d’écran 2024-10-14 à 17 21 46

REPL

We probably need a small wrapper to filter some stuff out. I'm going with something like this:

type SvelteAttributes<T> = {
  [K in keyof T as K extends `on:${string}` | `bind:${string}` | "children" ? never : K]?: T[K];
};

export type Attributes<T extends HTMLElementTagNameMap> = SvelteAttributes<SvelteHTMLElements[T]>;