-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Add a (singular) $prop
rune
#9241
Comments
$prop
rune$prop
rune
Would it be possible for this to work with a type declaration directly on the variable rather than a generic? E.g.: let someProp: string = $prop('default'); |
If "hijacking" TypeScript's syntax is an option, I also think this should be a considered syntax: let {
foo: string = 'default',
bar: number,
buzz: number = 123,
} = $props(); No more duplicate prop names, and the order of |
Generally not, that sort of annotation will only be checked for compatibility with the value being assigned, not change the type. @Snailedlt That should further simplify to: let {
foo = 'default',
bar: number,
buzz = 123,
} = $props(); |
@Snailedlt That syntax is already reserved by JavaScript for specifying aliases for the destructured properties. It can't be used for specifying types. |
Actually, one problem with the singular let classProp: string;
export { classProp as class }; And with the plural let { class: classProp } = $props(); But it doesn't seem like you'd be able to do the same thing with the singular |
Edit: The following is now an error. You can use let { foo = 'default' } = $props<{ foo: string }>();
let { bar } = $props<{ bar: number }>();
let { buzz = 123 } = $props<{ buzz: number }>(); Bit verbose, though. |
@brunnerh But the prop names are still being repeated, which is the main problem. |
@aradalvand Oh, didn't know that, how about this then? let {
foo<string> = 'default',
bar<number>,
buzz<number> = 123,
} = $props(); |
@Snailedlt Again, invalid syntax. |
What do you think about this syntax? const klass = $prop<string>("class");
const htmlFor = $prop<string>("for");
const myNumber = $prop<number>("my-number"); // also kebab-case prop names would be possible <MyComponent class="something" for="input-1" my-number={123} />
|
@TheHadiAhmadi Could work. We could also just use let klass = $prop<string>('class') ?? 'default'; Update: I think |
I'm not even using TypeScript and would prefer having all props neatly separated. In the same way that I would never do I'll throw the following in the mix, why not (runes seem to be bending what's considered JavaScript anyway and declaring prop names as strings does not sit well with me): const regularRequired = $prop<string>();
const reservedClassKeyword = $prop.class<string>();
const withDefault = $prop<number>(10); But I kind of hate it, you can't immediately see the props this component receives, you need to pay attention to the What if const regularRequired = $prop<string>();
const { class: reservedClassKeyword } = $prop<string>();
const withDefault = $prop<number>(10); |
We did not add it yet for a couple of reasons:
|
This can be hidden in |
Is it possible the priorities and trade offs decided are just wrong? Writing a TypeScript component is really common, exporting Making the stuff I read and write every single time I open a file in a TypeScript project more difficult in exchange for making something I seldom do a little less kludgy (you still have to alias it after all), is a horrible trade off. |
It's really not, I do that all the time when wrapping base elements. |
I strenuously disagree with both of those assertions — I would refer you to #6972. |
I feel like we're getting closer, but do we need to restrict ourself to only one "rune" for this task? As popular as reserved words may be in some cases (I agree "class" certainly is - and I don't think it should be considered an antipattern), for the most part they're still an edge case, so maybe better not to pollute the behavior of the default non-aliased const regularRequired = $prop<string>();
const regularWithDefault = $prop<number>(10);
const aliasedRequired = $aliasedProp('class')<string>();
const aliasedWithDefault = $aliasedProp('catch')<boolean>(false); An alternative signature for |
Agree with having |
How about the following: const regularRequired = $prop<string>();
const regularWithDefault = $prop(10);
const aliasedRequired = $prop<string>().as('class');
const aliasedWithDefault = $prop(false).as('catch'); Since the exposed property names for the component need to be statically analyzed anyway, we can do pretty much anything, as long as it's valid js syntax. |
When it comes to this discussion it's also probably worthwhile to check out the unofficial experimental |
That is not quite correct. In terms of types, runes expose the underlying values, so you cannot have chaining like that and still preserve the simple case without it. E.g. |
Ah, you are right, I did not take the types into consideration. Then let me throw in another idea, which is a somewhat revised version of yours and takes additional influences from the Vue const aliasedRequired = $prop<string>(undefined, {
name: 'class'
})
const aliasedWithDefault = $prop(false, {
name: 'catch'
}) The main advantages are, that there is no need for an additional rune and, at least from my point of view, it's highly readable and intuitive what's going on. The obvious disadvantage is that the syntax is somewhat more verbose, especially having to add Another advantage is, that this syntax is extendable for future features, e.g. a runtime validation, similar to Vue |
One thing I'm not certain about with this syntax is, without Typescript, how is Svelte supposed to know that |
Good point regarding the default/required issue. declare function $prop<T = undefined>(options?: any): T;
let required = $prop<number>();
let withDefault = $prop<number>() ?? 42;
let withDefaultImplicit = $prop() ?? 42;
let aliasedRequired = $prop<string>({ name: 'class '});
let aliasedWithDefault = $prop({ name: 'class '}) ?? 'flex';
// ?? removes undefined and null from the type
let requiredAllowUndefined = $prop<number | undefined>();
let requiredAllowNull = $prop<number | null>();
let withDefaultAllowUndefined = $prop() ?? (42 as number | undefined);
let withDefaultAllowNull = $prop() ?? (42 as number | null); |
|
@Not-Jayden : I like it. But two remarks :
Perhaps something like that let {
foo = $prop<string>(), // foo is a optionnal string (undefined by default)
bar = $prop<number>(42), // bar is an optionnal number (42 by default)
baz = $prop<string>('The Answer'), // baz is an optionnal string ('The Answer' by default)
id = $req<string>(), // baw is a required string
anotherProp, // anotherProp is typed based on TypeOfOthersProps
...props // others props, typed using TypeOfOthersProps
} : TypeOfOthersProps = $props(); would be equivalent to the following code : let {
foo,
bar = 42,
baz = 'The Answer',
id,
anotherProp,
...props
} : {
foo?: string,
bar?: number,
baz?: string,
id: string
} & TypeOfOthersProps = $props(); This could work... |
@adiguba Good considerations, didn't really think through the edges of it too hard. I think we both had different assumptions of how the type would be resolved, which might hint that the behaviour is a bit too magical. My expectation would be that Also I think it starts to break down if you try mix the type annotation after the destructured object with the let {
foo = $prop<string>(), // required string
bar = $prop<number>(42), // optional number
baz = $prop<string>('The Answer'), // optional string
id = $prop.optional<number>(), // optional number (i.e. type of id is number | undefined, but you can't pass undefined as a prop value)
props = $prop.rest<OtherProps>(), // rest of the props
} = $props(); Alternatively (additionally?) maybe there could be something like let {
foo = $prop<string>(), // required string
bar = $prop<number>(42), // optional number
baz = $prop<string>('The Answer'), // optional string
id, // optional number (via $prop.Type)
...props, // {qux: string} (via $prop.Type)
}: $props.Type<{id?: number, qux: string}> = $props();
// $prop.Type<{id?: number, qux: string}> is equivalent to
// {foo: string, bar?: number, baz?: string} & {id?: number, qux: string} Feeling a little less convinced of this approach with the additional complexity involved though. |
I recently started using the new Svelte 5 version and bumped into this Some of the props like But lets see this simple example bellow: <!-- component.svelte -->
<script lang="ts">
let { data, children } = $props()
</script>
<div>
<h1>{data.title}<h1>
{@render children()}
</div> It works as expected and the types are correct, no additional work is required. How to deal it when you only need to type one or two <!-- component.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte'
import type { ContentPages, ContentMenu } from '$types'
type Props = {
data: {
title: string
pages: ContentPages[]
menu: ContentMenu
// ...
}
children: Snippet
x: number
y: string
}
let { data, children, x, y }: Props = $props()
</script> As you can see above, I now have to explicitly type all the It's not a big deal if you have a few components, but when you have a lot of components and more complex nested As the already suggested above, one should definitely consider all the options that exist to be able to integrate the individual use of props. <!-- component.svelte -->
<script lang="ts">
let data = $prop() // automatically infer types
let children = $prop() // automatically infer types
let x: number = $prop() // explicitly sets the type
let y: string = $prop() // explicitly sets the type
</script> or <!-- component.svelte -->
<script lang="ts">
let {
data = $prop(), // automatically infer types
children = $prop(), // automatically infer types
x = $prop<number>(), // explicitly sets the type
y = $prop<string>(), // explicitly sets the type
} = $props()
</script> Mostly the syntax is less important in this case, it is important that |
For aliasing I like to propose the following syntax: <script lang="ts">
let className: string | undefined = $prop["class"](undefined); But only allow that for the limited set of javascript reserved keywords. |
Maybe off topic and some freak bot definitely hide this comment. But, I gotta say this "Thanks for making svelte unusable with this runes and crazy things, svelte used to be great with svelte 4". struggling to rewrite my entire component library with $props rune and typescript. |
You've probably not reached the point where runes makes things easier |
I've put together a preprocessor that implements a version of this rune: https://github.com/ottomated/svelte5-prop-rune Some examples: const simple: string = $prop();
const optional: string = $prop('fallback value');
const value: number = $prop.bindable();
const class_: string = $prop().as('class');
const rest: HTMLButtonAttributes = $prop.rest(); I think this addresses your concerns, @dummdidumm -
I think this proposal is clean and type-safe. Open to feedback.
This allows for rest props and aliases without refactoring. Having a component implement an interface seems rare, but I think unavoidable if $prop and $props are exclusive.
Agree that this would be pretty complicated, but I don't think that's an excuse for not doing it :) I would be happy to help on any development efforts around this. |
This is soooo much more boilerplate... I dont understand how this is would benefit |
To add more weight the option of doing nothing, it's worth noting maybe this problem will be solved in TypeScript-land one day if this discussion ever progresses microsoft/TypeScript#29526 i.e. one day it might just be able to be typed like this let {
foo<string>,
bar<number?> = 42,
class<string?>: className,
} = $props(); but I'm certainly not holding my breath. |
It is theoretically possible that TypeScript may eventually solve this problem on their end, but I wouldn't count that as a particularly meaningful argument in favour of doing nothing in Svelte. That particular TypeScript discussion has been open since 2019 and seems to be stuck in limbo for the foreseeable future. There seems to be no clear consensus on the syntax and as far as I can tell the TypeScript development team has never acknowledged or commented on any of the proposals in that discussion. |
@Rich-Harris I'd like to give implementing this a shot, do you have any comments on this spec? |
Please don't — we have no intention of changing how props work. The lack of a singular With the current design, you can use your existing JavaScript/TypeScript knowledge. Need to rename a prop? You can do That all stands in contrast to the various proposals here, which involve new runes and new rules. It gets particularly complicated if you allow It's worth noting that while the example above... const simple: string = $prop();
const optional: string = $prop('fallback value');
const value: number = $prop.bindable();
const class_: string = $prop().as('class');
const rest: HTMLButtonAttributes = $prop.rest(); ...is fewer lines than today's version with Prettier applied... const {
simple,
optional = 'fallback value',
value = $bindable(),
class: class_,
...rest
}: {
simple: string;
optional?: string;
value: number;
class: string;
} & HTMLButtonAttributes = $props(); ...it's actually more characters. When I look at the first one, it takes more effort to understand which non-rest props exist — I need to look at the variable declaration and also check to see if there's an And of course I can do this sort of thing, and reference const {
- simple,
optional = 'fallback value',
value = $bindable(),
- class: class_,
...props
}: {
simple: string;
optional?: string;
value: number;
class: string;
} & HTMLButtonAttributes = $props(); In fact (apologies for not updating the thread with this change), a while back we made it possible to forego the destructuring altogether in the common case that you don't need optional or bindable values, so in many cases you can do this sort of thing: let props: { a: string; b: number } = $props();
// exactly equivalent to this
let { ...props }: { a: string; b: number } = $props(); To reiterate a point that has been made elsewhere: this is just how TypeScript works. Every single time you declare a function with destructured arguments anywhere in your codebase, you have to do the exact same thing (but worse, if you use explicit return types): function foo({
a = 1,
b = 2,
c = 3
}: {
a: number;
b: number;
c: number;
}): {
d: number;
e: number;
f: number;
} {
return {
d: a * 2,
e: b * 2,
f: c * 2
};
} Deviating from that by adding a bunch of new stuff to learn (that doesn't have as much expressive power) really doesn't seem like a good choice. (Though I do take the point that it would be nice to infer things like |
I don’t know about other Typescript users, but as you say, its interactions with destructuring are bad - in my case this means I very rarely use destructuring in TS code. I’d be a shame for Svelte 5 to make it a mandatory part of how components work. |
Glad to get an official statement on this! I can't say I agree with all your reasoning (neither line nor character count are a great measure of readability or writability), and this does feel like an explicit DX downgrade from svelte 4, but I understand the case for only allowing one option. I do think it's a shame that this can't be implemented in userland because of this issue sveltejs/language-tools#1567. I also am curious about real-world usage—I expect that the vast majority of components only use one or two props (the case where |
@Rich-Harris Thanks for the explanation. I agree that having one standard is better than having two, but like someone mentioned already, the DX for $props feels like a significant downgrade from Svelte 4's simple export let prop declaration. I still don't fully understand why we had to go away from the superior DX of using export let. It's so simple and elegant compared to the $props rune. Afaik the only downside with export let is that you have to do some weird stuff to get typescript to play along... but I don't understand what the technical reason for that is. |
More characters but way nicer to read IMO. |
It all comes down to personal preferences. |
Ok fine.
You might say that each of these issues is a relatively minor or infrequent concern, and I would believe you. But when you add all these things up, and especially if you approach it from the perspective of someone new to the framework, it becomes clear that it's a bad design. Just to provide some context: we have been actively discussing As such, I'm going to close this issue. |
Hi @Rich-Harris, would you kindly consider dropping They also have entirely different semantics compared to their vanilla JavaScript counterparts, and feel weird as well: #12778 (comment) |
Thank you for the explanation, that clarified a few things!
All that said I'm glad to see discriminated unions getting some love, as it is something I've been struggling with personally (though not in Svelte). Sidenote: |
I think this is a very valid point. It is great that the Svelte team has been discussing this issue internally and has considered the tradeoffs carefully, but I for one would really appreciate if these discussion were more visible to the community. I have been following this issue for months, but the response from Rich Harris 2 days ago is the first time that I have seen any acknowledgement that the Svelte team is even aware of this issue. |
That's fair, I had missed those two comments. Though there does seem to be a pretty big gap in communication between "We did not add it yet for a couple of reasons." from September last year to "We have no intention of changing how props work. The lack of a singular $prop rune isn't an oversight, it's the result of many, many hours of careful deliberation." |
I want to give my 2 cents about the wish that the Svelte team would involve the community more in their (normally internal) discussions about how to design the Svelte language, especially before they actually make a final decision. A couple years ago I probably would've agreed. I too deeply appreciate and care about syntax that is easy on the eyes, contains as little noise possible, so that there are minimal distractions when trying to read code and understand what it does. (click to read my explanation about how I get to my conclusion)In the past, I've made feature requests for various libraries, believing I had solutions to improve their API so that the consumers of their library would automatically be able to write code that was very easy to read and understand. However, contributors often explained how my suggestions would create more problems than they solved. These experiences taught me that my understanding of complex libraries and frameworks is limited compared to long-time contributors. What seems suboptimal for simple use cases often proves invaluable in more complex scenarios. I've noticed similar patterns in this thread. Some confidently propose syntax changes, while team members like @Rich-Harris or @dummdidumm expertly identify multiple issues with these suggestions. This consistently demonstrates the Svelte team's deeper understanding of the language and the far-reaching implications of design choices. These interactions are both humbling and enlightening, reinforcing the value of the team's expertise and experience. (Btw, yes I used AI to write that, because what I had written here originally was terribly long-winded / bloated.) That's why I don't think that the Svelte Team should always involve the community in their decision-making process. The team's expertise likely surpasses most suggestions from the community, and involving the community in every decision would significantly slow down development. The team's time is better spent making informed decisions and writing code for Svelte, rather than managing extensive community input for each choice. |
I don't really disagree. I can certainly see the value in having a more streamlined decision making process that doesn't involve the community in every step. However, I do wish that the Svelte team would communicate their internal discussions and decisions more often and more publicly, so that the broader community could see where they stand on contentious issues and could consequently spend less time and energy on discussions and proposals that ultimately won't influence the direction of Svelte. |
Yeah, sounds fair. Though that would really only work if the community can actually accept that when the Svelte team communicates about a design decision they've made including how they got to their final decision, it's really just an update, and not an invitation to further discuss the topic. Or maybe they could just mention in their post something like:
|
I see no issue with saying "this is our decision, and we just want you to inform you about it". I just want them to be able to justify their decisions (like Rich did in his recent comments), and let the community know that this is what they think is the best for Svelte moving forward. The community will comment on this given the chance, and the Svelte team is free to ignore it, discuss it, consider it, or do whatever else they'd like with the feedback they're given. Either way I still think it's valuable to have the decisionmaking process be public, and give the community a place to voice their opinion about recent changes. |
Describe the problem
This came up multiple times in the Discord server, so I figured I'd create an issue for it:
In v5's runes mode, the only way to declare props is by using the
$props
rune, like so:This looks good unless you're using TypeScript, in which case in order to type your props, you'll have to pass a type argument to
$props
, what this means in effect is that you'll be duplicating your prop names:Needless to say this is not very pretty, feels a bit overly verbose, it gets even more problematic when you have many props:
Here, you have to traverse quite a bit of distance with your eyes in order to find a specific prop's type (e.g.
bar
), because the destructured variable and the corresponding type could potentially be far from each other, and you'll have to "switch" your eyes to an entirely different "list", if you will.In addition, this format yields a weird order of
name
=>default-value
=>name
=>type
, and when you compare all of this to the old way of declaring props with types, the difference becomes apparent:Which is more concise, of course, but the order is more logical as well:
name
=>type
=>default-value
.Describe the proposed solution
Introduce a singular
$prop
rune, like so:Which, once again, is less verbose and has a more logical order of symbols; the type of each prop is always next to it on the same line, and you don't have to repeat the prop name.
Alternatives considered
$props
rune, which suffers from the aforementioned problems.Importance
nice to have
The text was updated successfully, but these errors were encountered: