-
-
Notifications
You must be signed in to change notification settings - Fork 200
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
Typing Svelte Component Props/Events/Slots [Feedback Thread] #442
Comments
Seems reasonable, there is actually a Svelte issue about being able to inject slots at runtime. sveltejs/svelte#2588 and a PR that implements it sveltejs/svelte#4296, feels like there might be overlap, or at least some opportunity to align the interfaces (if there is any consenus, there are still some outstanding questions with the above PR). |
Thanks for the PR link, seems like it's only related in a way to us that we have to type the constructor a bit differently then, but I think this is indendent of the type checking on a template level. |
interesting. <script lang="ts" generic="T">
type T = unknown
export let items: T[]
let item:T = items[0]
</script>
<slot b={item}></slot> which would strip the //...
render<T>() {
export let items: T[]
let item:T = items[0]
} |
Good idea about adding it to the I think we can get the same results with
by extracting the Although with your solution you will have to type less in the case of "I just want a generic relationship between my props and slots", which is nice. On the one hand I'm thinking "yeah we could just add both", on the other hand it feels a little like expanding the API surface too much (you can do the same things in different ways) - not sure. |
I really don't want to have to write out the I like @halfnelson's line of thinking. Exports should be detected as props. I don't know what the module looks like once <script> is transpiled to a module, nor how the Svelte Language Server gets to see it when consuming it from another file, but I'd really hope for it to be possible for exports to just be detected automatically as props. I have to dash now, but one more thought: As for typing the slot, would JSX generics provide any inspiration? |
Another quick one, as I read it in some of the related issues: I've had no end of trouble using JSDoc comments to type things, including the While we should keep an open mind to JSDoc (even using JSDoc within the HTML), I must warn that, whether used through WebStorm or VS Code, it is simply not expressive as TypeScript. e.g. it does not implement the |
Passing the generic type the (jsx)element is a no-go for me because it's not valid svelte template syntax. It should also not be needed since generics are driven by properties, I cannot think of a way to introduce a generic dependent for slots/events only. If they are driven by input properties passing generics to jsx elements is not needed because we can generate About the typing overhead: This is correct but would only be needed in situations where you want to use generics and/or ecplicitly type events/slots. In theory, we could infer all of this ourselves, but this feels super hard to implement. Examples of such hard to implement problems are support for advanced slots scenarios like in sveltejs/svelte#263, and to collect all possible component events (which is easy if the user only would use In general I feel that there are many situations where people would want only to define one of those things, but not the others, so maybe it is indeed necessary to provide all the different options to keep the "type less"-spirit of Svelte. This would mean:
|
@shirakaba The JSDoc type support doesn't have to be as feature-rich as a typescript. I doubt a lot of people would type generic components with it. Also because we're using typescript's language service, a lot of the advanced type can be easily imported from a typescript file. About slot props type-check, I have some hack but don't know if this would lead to good developer experience. If user type component like this: interface ComponentDef {
slots: { default: { item: string } }
} we can generate a class class DefaultSlot extends Svelte2TsxComponent<ComponentDef['slots']['default']>{ } and transform default slot to
|
(only chiming in as a user) For me the extra typing is not an issue because I have to do a lot of it anyway while working with TypeScript. As for the interface vs the |
This is invalid TS syntax which might confuse people. Also, we would have to do an additional transformation step, which would not be done in case the Svelte component is in a non-compilable-state. In this case the language-tools fall back to what is in the script, which then would have this invalid syntax. So I'm against that. |
I understand. Not to linger, but is it invalid syntax or more of an "undefined/unknown type" error? I'm asking because I don't know how TypeScript's compiler handles the two cases. I just have reservations against adding a custom attribute to a All in all, for me, this is a nice-to-have and perhaps forcing the user to type the exported variables when using the component is a good thing (more readable code). |
When you type The alternative would be to type the whole component via For me it becomes clear from the discussion that people have very different opinions on this and to have more options laid out ( |
I may have missed this but is splitting it up an option? <script>
import type { Foo } from './foo';
export let items: Foo[];
interface ComponentSlots<T> {}
interface ComponentEvents<T> {}
</script> Would that reduce the typing overhead for most cases? |
Yes this would be possible. |
@dummdidumm is it possible to work on supporting |
It wouldn't, but before continuing the implementation I first want to write a RFC to get more feedback from the community and the other maintainers on the proposed solution. Once there's an agreement, we will start implementing those things. |
@joelmukuthu btw why would you like to have the interface support? We enhanced the type inference for slots, is there a case where it's still not working out for you? If so, I'm curious to hear your case. |
I now created a RFC on this topic: sveltejs/rfcs#38 |
@dummdidumm sorry for the slow response. I've just realised that for my use-case, I do actually need support for generics. Type inference for slots works quite well! |
This would be huge! I'm currently saddened that using slot properties is always an any. |
That's strange, slot typed can be infered quite nicely at this point, if the compont you use is within you project and uses TS, too. |
Perhaps I'm using the wrong terms here... or hopefully I just did something wrong. Example of what I wish I could do: <SelectInput options={myArrayOfObjects} let:option>
<!-- I wish option here was the correct type */ -->
{option.myLabelProp}
</SelectInput>
<!-- SelectInput Component -->
<script lang="ts" generic="T">
export let options: Array<T> = [];
</script>
<div class="select">
{#each options as option}
<slot {option} />
{/each}
</div> |
Ok I understand, yes right now this is not possible, so you have to fall back to |
A nice temporary solution to type checking components I've come across in "Svelte and Sapper in Action" by Mark Volkmann is the use of the React prop-types library alongside $$props. This is the code for a Todo item for example:
While I'm validating the props of Todo and getting errors if something is off, I also want to have an interface for Todo items, so that I can have static type checking in VSCode as well as autocompletions. This is achieved by using the interface I defined above. The obvious and significant downside of this is that I need to have duplicate code here. I can't define an interface from the propTypes object, or vice-versa. I'm still starting out in javascript so please correct my if any of this is wrong. I think type checking is a must have for any productive codebase, both static checking and props type checking at runtime. (Note that the interface would be defined in context='module' so its exportable alongside the component default export, but omitted that part for brevity) Edit: Have not tried this for anything other than props validation, let me know if it would also work for slots/events! |
This was based on the ideas here sveltejs/language-tools#442 (comment) (search for `absolute bonkers`
Did you found the solution? I build something similar, lol |
@axmad386 it's worked with me after some svelte update. I didn't change anything. |
In the RFC, it's mentioned that you probably won't use ($$Props) because it's essentially doing the type work twice, however, I think there is a valid use case here that is being missed. As an author of a component library written in Svelte, I'd like for my components to accept any valid HTML attributes and forward them to the HTML Element that the component renders. The way I've decided to implement this matches the way I would in other frameworks, which is typing the component as an extension of an existing interface that encompasses valid HTML Attributes, yet not explicitly defining any of those attributes as props on the Component itself. <script lang="ts">
interface $$Props extends svelte.JSX.HTMLAttributes<HTMLButtonElement> {
error: boolean;
}
export let error: boolean;
</script>
<button class:error {...$$restProps}>
<slot />
</button> <!-- id and data-bar are forwarded to the <button /> as well as strongly typed -->
<Button error={false} id="foo" data-bar="baz">Click Me</Button> Because the <script lang="ts">
interface $$Props extends svelte.JSX.HTMLAttributes<HTMLButtonElement> {
error: boolean; // error is explicitly typed as boolean to consumers of the component
}
export let error; // error is implicitly typed as any within the component
</script>
<button class:error {...$$restProps}>
<slot />
</button> Allowing components to safely forward valid HTML Attributes to the HTML Element they render is a fairly common use case amongst component library authors. I would love to see some more robust support for this. Here's how it looks in React for example: import * as React from 'react';
interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
error: boolean; // error is explicitly typed as boolean to consumers of the component
}
// error is explicitly typed as boolean within the component
const Button: React.FC<ButtonProps> = ({ error, children, ...restProps }) => (
<button className={error && 'error'} {...restProps}>
{children}
</button>
);
export default Button; To summarize, I would love a way to simultaneously define and type a Svelte Component's external API and internal props en masse without duplicating a type. Thanks. |
Hi everyone! I do this by this way <script lang="ts">
interface $$Props extends svelte.JSX.HTMLAttributes<HTMLButtonElement> {
error: boolean; // error is explicitly typed as boolean to consumers of the component
}
let {error, ...restProps} = $$props as $$Props;
</script>
<button class:error {...restProps}>
<slot />
</button> Of corse, it is tricky, because we are casting SvelteAllProps to $$Props, but it close to safe way to do it. |
How to extend prop types from HTML elementsFor everyone who wants to create a wrapper component around a certain HTML element and wants a way to type "this component accepts all properties of X", here's how you do it as of Svelte version 3.55: <script lang="ts">
import type { HTMLButtonAttributes } from 'svelte/elements';
interface $$Props extends HTMLButtonAttributes {
error: boolean; // your own additional typings
}
export let error: boolean;
// ...
</script>
<!-- ... -->
<button>
<slot />
</button> Svelte version 3.55 comes with a new |
Regarding the HTML props feature, it would be nice if duplication of all the custom props could be avoided. <script lang="ts">
import type { HTMLButtonAttributes } from 'svelte/elements';
interface $$Props extends HTMLButtonAttributes, $$ExportedProps {
// `error` not specified explicitly
}
export let error: boolean;
</script> Or just: type $$Props = HTMLButtonAttributes & $$ExportedProps; |
@brunnerh Either the latter would be my vote, or, if we could simply infer the HTML element we're passing the $$restProps to, i.e. <!-- Image.svelte -->
<img {...$$restProps} />
<!-- App.svelte -->
<script>
import Image from './image.svelte'
</script>
<!-- Type-safe, the following would error as missing a `src` -->
<Image /> Side benefit of forcing the |
It's not either or; once the type is there, you can use it both ways, that's just TS. You would usually only use the |
Not necessarily, only if you want to add props. Example without wanting to add props I can think of is an <!-- UnstyledLink.svelte -->
<a {...$$restProps}><slot /></a>
<style>
a {
text-decoration: none;
color: inherit;
}
</style> I'd then like the caller to automatically have to supply an |
You misunderstood what I was saying. You said:
My point was, it's not either "the former" or "the latter", but that both can be used. Of course my suggestion does not include the automatic inference you detail after that. So your statement might as well have been:
Also, it probably would not hurt to have both the |
Is there a way to have an optional added prop with the My example:
This fails with error:
Is there a recommended way to provide an optional prop and type it in $$Props? Marking it as required and allowing EDIT: I was able to fix this by defining the prop as follows:
Leaving the question above to help others who might run into this. |
Types like this don't seem to be working either: type BaseProps<T extends HTMLElement = HTMLElement> = HTMLAttributes<T> & {
[key: `data-${string}`]: string | boolean | undefined;
}; |
I commented on the RFC because it was more recent, but this thread is where I should have posted in the first place. Sorry for the spam for people in both threads. I use generics to have events typed using a children component's props, it looks roughly like this: <script lang="ts">
type Entry = $$Generic<
ComponentProps<EntryComponent> & {
id: string;
}
>;
export let entries: Entry[];
const dispatch = createEventDispatcher<{
click: Entry;
}>();
</script>
{#each entries as entry (entry.id)}
<EntryComponent {...entry} on:click={() => dispatch('click', entry)} />
{/each} I don't know how much of a pattern/anti-pattern this is, but it allows for complete type inference without hand-maintained types |
As far as I can tell, the current const getKeys = <const T extends { key: string }>(items: T[]): T['key'] => {
return items[0].key
};
// keys has type type "foo" | "bar"
const keys = getKeys([{ key: 'foo' }, { key: 'bar' }]);
// keys2 has type "baz" | "qux"
const keys2 = getKeys([{ key: 'bar' }, { key: 'qux' }]); This means it's impossible to strongly type one prop with the values of another, complex prop. |
That's correct - for this reason and a couple of others we will probably switch to another way of defining generics: #2020 |
HI - I'm a bit confused why this doesn't exist in the documentation. This seems to be a supported feature of sveltekit (the compiler can certainly tell that I'm typing my slots, and gives type errors / warnings etc.), but it's nowhere to be found on the website. Using: @sveltejs/kit: 1.20.4 I eventually found this functionality detailed at this link, but it should definitely be in the official documentation. |
It's experimental still, which is why it's not in the official documentation yet - but there will be a link to the RFC on the site, soon |
@dummdidumm awesome, thank you! |
Svelte v4 breaks the pattern I described above: <script lang="ts" context="module">
type MinimalThing = {
name: string;
};
</script>
<script lang="ts" generics="Thing extends MinimalThing">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher<{
click: Thing;
}>();
export let thing: Thing;
</script>
<button on:click={() => dispatch('click', thing)}>{thing.name}</button>
<!-- ~~~~~~~~~~~~~~ -->
I don't know how to fix this Edit: tracked in sveltejs/svelte#8860 and microsoft/TypeScript#54806 |
This is possible now - cool! see sveltejs/language-tools#442 (comment) 'mat' shorthand attribute will give us proper intellisense (props list) for the assigned 'material'!
There appears to be an issue with angle brackets in the <script lang="ts" generics="T extends Record<string, unknown>">
Can be worked around by extracting the base type as a type alias to a |
<script lang="ts" generics="Foo, Bar extends Record<string, unknown>"> is the official forward here, and docs will be on the new Svelte site soon, therefore closing. |
Here is a way to create a component that can be a button or an anchor tag. (Or any other element) <script lang="ts" generics="E extends keyof svelteHTML.IntrinsicElements = 'button'">
import type { Snippet } from 'svelte';
import type { SvelteHTMLElements } from 'svelte/elements';
type Props<E extends keyof svelteHTML.IntrinsicElements> = {
as: E;
children: Snippet;
} & SvelteHTMLElements[E];
let { as, children, ...rest }: Props<E> = $props();
</script>
<svelte:element this={as} {...rest}>
{@render children()}
</svelte:element> |
[this is now the feedback thread]
This was implemented according to this RFC. Please provide feedback about using it in this thread.
Original Post
There are several issues about this already (#424, sveltejs/svelte#304, sveltejs/svelte#273, sveltejs/svelte#263), but I wanted to make a consolidated one to have a discussion about the approaches we can take.
Related PR: sveltejs/svelte#437
Note that this issue is NOT about typing a
d.ts
file, this will come afterwards as a separate issue.Is your feature request related to a problem? Please describe.
At the moment it is not possible to type the inputs/outputs of a component under certain circumstances:
Describe the solution you'd like
A way to type props/events/slots explicitly.
Proposal: A new reserved interface
ComponentDef
which, when defined, is used as the public API of the component instead of infering the things from the code.Example (with comments about what likely is not possible):
When someone now uses the component, he will get the correct types from the props/events/slots and also error diagnostics when something is wrong:
If this works and is tested a little, we could enhance it with other reserved interfaces like
ComponentEvents
to only type one of the things.I'd like to get as much feedback on this as possible because this is likely a big change which might be used broadly. Also, I'd like to have some of the core team members to have a look at this if this looks good to them or introduces something they don't agree with.
@jasonlyu123 @orta @Conduitry @antony @pngwn
The text was updated successfully, but these errors were encountered: