sveltejs / language-tools

The Svelte Language Server, and official extensions which use it
MIT License
1.25k stars 200 forks source link

Slot props not given TypeScript types when nested in <svelte:self> #1852

Open fritz-c opened 1 year ago

fritz-c commented 1 year ago

Describe the bug

Normally, when I pass a typed prop to a slot, components passing in an element to that slot can access a typed copy of that prop from the parent.

Example:

<!-- App.svelte -->
<script lang="ts">
  import Doubler from './Doubler.svelte';
</script>

<Doubler value={25}>
  <!--                               vvvvvvv   this has the type of `number` -->
  <strong slot="myslot" let:doubled>{doubled}</strong>
</Doubler>

<!-- Doubler.svelte -->
<script lang="ts">
  export let value: number;
  $: doubled = value * 2;
</script>

<strong>
  <slot name="myslot" {doubled} />
</strong>

But I noticed one case where the slot props do not get types as expected. I was creating a component to represent tree structures that allows for custom rendering of each node using slots (see my reproduction repo for the slimmed-down code). It uses <svelte:self> to render deeper levels of the tree recursively (very similar to the tutorial on svelte:self, but allowing for custom rendering of the node). In that case, any slots placed inside the <svelte:self> do not retain the types of the props, and end up as any.

Reproduction

Here's a repo from a Skeleton project sveltekit build with the issue demonstrated:

https://github.com/fritz-c/svelte-nested-slot-type-issue

Here is the commit with the changes I made isolated:

https://github.com/fritz-c/svelte-nested-slot-type-issue/commit/906dde5f57cd066aeb3db90bcc075eef4d1bc6cf

Logs

No response

System Info

System:
    OS: macOS 13.1
    CPU: (8) x64 Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz
    Memory: 2.81 GB / 32.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 16.16.0 - ~/.volta/tools/image/node/16.16.0/bin/node
    Yarn: 1.22.11 - ~/.volta/tools/image/yarn/1.22.11/bin/yarn
    npm: 8.11.0 - ~/.volta/tools/image/node/16.16.0/bin/npm
  Browsers:
    Chrome: 109.0.5414.87
    Firefox: 106.0.5
    Safari: 16.2
  npmPackages:
    svelte: ^3.54.0 => 3.55.1

Severity

annoyance

webJose commented 8 months ago

I stumbled upon this myself. I'll explain my case.

Recursive Component Example

The following code works, even with the commented lines uncommented.

<script lang="ts" context="module">
  export type NodeItem<T extends NodeItem<T>> = {
    id: number;
    text: string;
    nodes?: T[];
  }
</script>

<script lang="ts" generics="TItem extends NodeItem<TItem>">
  export let nodes: TItem[];
  $: console.log('Nodes: %o', nodes);
  $: console.log('Slots: %o', $$slots);
</script>

<ul>
  {#each nodes as node (node.id)}
    <li>
      <slot {node}>
        <span>{node.text}</span>
      </slot>
      {#if (node?.nodes?.length ?? 0) > 0}
        <svelte:self nodes={node.nodes} let:node={subNode}>
          <!-- <slot node={subNode}> -->
              <span>{subNode.text}</span>
          <!-- </slot> -->
        </svelte:self>
      {/if}
    </li>
  {/each}
</ul>

This (with the commented lines still commented), produces the following type for the component, as per VS Code's tooltip:

function render<TItem extends NodeItem<TItem>>(): {
    props: {
        nodes: TItem[];
    };
    slots: {
        default: {
            node: TItem;
        };
    };
    events: {};
}

Note how the node variable is correctly inferred to be of type TItem.

Now, if we uncomment the commented lines so the slotted content is passed down the recursion, the type changes to any:

function render<TItem extends NodeItem<TItem>>(): {
    props: {
        nodes: TItem[];
    };
    slots: {
        default: {
            node: any;
        };
    };
    events: {};
}

This version still works properly, and I even get the desired result: The slot content specified by the consumer of the component is correctly used down in the recursion.

If I modify the component to provide separate slots (one for the root element and one for the recursion part), then the node variable regains its correct type of TItem, and the new slot's node variable is of type any. So I guess this hints to the fact that the problem is only inside <svelte:self>, as the OP of this issue originally stated.

Desired Result

My desired result would be to not lose the correct data type when <svelte:self> is used.


I'll continue my research, as my ultimate goal is to create specialized variations of this base component, and I suspect it might reveal more problems.

Thank you, team, for your efforts making this amazing framework possible.