sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
78.8k stars 4.14k forks source link

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

Open kwangure opened 5 days ago

kwangure commented 5 days 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 5 days 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]>;