Skip to content

Restricting children to Specific Components in Svelte with TypeScript #16025

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
OTheNonE opened this issue May 29, 2025 · 8 comments
Open

Restricting children to Specific Components in Svelte with TypeScript #16025

OTheNonE opened this issue May 29, 2025 · 8 comments

Comments

@OTheNonE
Copy link

Describe the problem

This is how you currently render the children within a component:

<!--my-component.svelte-->
<script lang="ts">
    import type { Snippet } from "svelte";

    type Props = {
        children?: Snippet
    }

    let { children }: Props = $props()

</script>

<div>
    My children are:
</div>

{@render children?.()}
<!--+page.svelte-->
<MyComponent>
  <p> Peter </p>
</MyComponent>

Although, using typescript, there is no way to define what the content of children within <MyComponent> should be.

Describe the proposed solution

I would like to define the type of children in the component, so that typescript only allows some specific elements or components to be children:

<!-- This should trigger an error in typescript. -->
<MyComponent>
  <p> Peter </p>
</MyComponent>

<!-- This should also trigger an error in typescript. -->
<MyComponent>
  <NotTheTypedComponentInMyComponent/>
</MyComponent>

<!-- This should be OK. -->
<MyComponent>
  <TheCorrectComponent/>
</MyComponent>

This should be done somehow similar to this:

<!--my-component.svelte-->
<script lang="ts">
    import MyComponent from "$lib/my-component.svelte"

    type Props = {
        children?: MyComponent // This does not work, but something similar to this.
    }

    let { children }: Props = $props()
    ...

Importance

would make my life easier

@7nik
Copy link
Contributor

7nik commented May 29, 2025

What is the point of limiting children type? Why your component cannot accept <p> Peter </p> or <NotTheTypedComponentInMyComponent/>?

@MotionlessTrain
Copy link
Contributor

If it can only accept a couple of components, it would make more sense to give the component as property instead, and have that in the correct place in the template instead. Then it can be typed much more easily

@paoloricciuti
Copy link
Member

What is the point of limiting children type? Why your component cannot accept <p> Peter </p> or <NotTheTypedComponentInMyComponent/>?

Sometimes it could be useful, if you build something like this for example you might want the children to only be TabItems because every other children will not work or even disrupt your component.

Someone might argue that this is not the right way of doing it but i think it could be useful sometimes. The real problem is that snippets don't "return" anything so it could be difficult to type check that.

Regardless this is more an issue for language-tools as svelte can't do much here.

@7nik
Copy link
Contributor

7nik commented May 29, 2025

Can it even be type-checked? Tab expects exactly TabItems in children, but for TS, there is no difference between <TabItem name="tab 1"> and <RandomComponent name="John"> with compatible set of props.

@MotionlessTrain
Copy link
Contributor

And what if ’s template actually is nothing else but a bunch of s?

@paoloricciuti
Copy link
Member

Yeah it's definitely not straight forward, probably not even doable.

@OTheNonE
Copy link
Author

I can let you in on my use-case:

I am creating a small svelte-wrapper for my implementation of OpenLayers in my app. This is a tiny somewhat simplified part of the code:

<!--+page.svelte-->
<script lang="ts">    
    import { Map, OpenStreetMap } from "$lib/ol/components"
</script>

<Map>
    <TileLayer>
        <OpenStreetMap/>
    </TileLayer>
</Map>
<!--map.svelte-->
<script lang="ts">
    import { setMapContext } from "./ctx";
    import type { Snippet } from "svelte";
    import Map from "ol/Map.js"
    import View from "ol/View";

    type Props = {
        children?: Snippet
    }

    const { children }: Props = $props()

    let map_element: HTMLDivElement
    let map = $state<Map>()
    
    setMapContext(() => map)

    const view = new View({ ... })

    $effect(() => {

        map = new Map({
            target: map_element,
            view
        })

        return () => map?.dispose()
    })

</script>


<div class="h-full" bind:this={map_element}>
    {#if map}
        {@render children?.()}
    {/if}
</div>
<!--tile-layer.svelte-->
<script lang="ts">
    import TileLayer from "ol/layer/Tile";
    import { OSM } from "ol/source";
    import { setTileLayerContext, getMapContext } from "./ctx";
    import Layer from "ol/layer/Layer";

    const layer = new TileLayer()
    
    setTileLayerContext(() => layer)

    const map = getMapContext()

    $effect(() => {
        
        map.addLayer(layer)
        
        return () => map.removeLayer(layer)
    })

</script>
<!--openstreetmap.svelte-->
<script lang="ts">
    import TileLayer from "ol/layer/Tile";
    import { OSM } from "ol/source";
    import { getTileLayerContext } from "./ctx";
    
    const source = new OSM()
    
    const layer = getTileLayerContext()

    $effect(() => {
        
        layer.setSource(source)
        
        return () => layer.removeLayer(source)
    })

</script>

In the map.svelte and tile-layer.svelte component, all components are allowed as children, eventhough it does not make sense for some components to be child of map. E.g. the class OSM() must be a child of TileLayer(), and TileLayer() must be child of Map(), through the method of .addLayer() and setSource() (enforced by Typescript). I was hoping that i could enforce the same restrictions within components childrens.

@7nik
Copy link
Contributor

7nik commented May 30, 2025

For now, the best you can do is throwing an error <X> must be inside <Y> when a component fails to get the right context.

@Rich-Harris Rich-Harris added this to the one day milestone Jun 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants