-
-
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
Class state has a fundamental typing flaw #12655
Comments
Related:
|
try export class Foo {
count = $state(0)
constructor(initialCount: number) {
this.count = initialCount
}
}
|
That's a hack more than anything (and a painful one for more complex types). As a workaround I would probably use a definite assignment assertion: export class Foo {
count: number = $state()!;
...
} |
Yes, that's the problem. It adds
This almost works, but unfortunately disables TypeScript's definite assignment analysis, which is a pretty huge issue -- it means you could easily introduce a logic branch into the constructor that causes your class field to be |
Related: #11116. An alternative to the current design that's been suggested is to allow export class Foo {
count: number;
constructor(initialCount) {
this.count = $state(initialCount);
}
} It would solve this problem, and make certain other things easier. The big downsides:
|
I've been doing this without complaint: export class Foo {
count = $state<number>();
constructor(initialCount) {
this.count = initialCount;
}
} |
@robert-puttshack Doesn't that disable the "this field isn't assigned in the constructor" checks, i.e. this example would not report an error despite being completely type-unsafe (
|
This is just the same as as having the type on the left: export class Foo {
count = $state<number>();
constructor(initialCount: any) {
this.count = initialCount;
}
get double() { return this.count * 2; } // Object is possibly 'undefined'. ts(2532)
} |
Could we do: class Foo {
count: number = $state(); // Property 'count' has no initializer and is not definitely assigned in the constructor.(2564)
constructor() {
}
} class Foo {
count: number | undefined = $state(undefined); // fine, `number` is `undefined`
constructor() {
}
} i.e. for class state fields, you have to explicitly set class Foo {
mustBeAssignedInCtor: number = $state();
mustBeAssignedInCtor: number; // what TS sees
defaultUndefined: number | undefined = $state(undefined);
defaultUndefined: number | undefined = undefined; // what TS sees
} Not sure of feasibility but this seems like the kind of thing we could provide excellent compiler help with, such as |
I'm concerned over the constructor approach, specifically over the second bullet point ( I sometimes only initialize my class once and then have lifecycle UPDATE: I've been persuaded to work with the grain of classes and the strange semantics of constructors. Using In many cases, though, I'll still likely do stuff like |
Another option: Currently, in class state field definitions, class Foo {
bar: number | undefined = $state();
}
// corresponds to, in TypeScript:
class Foo {
bar: number | undefined = undefined;
} But why can't it be this?: class Foo {
bar: number = $state();
}
// corresponds to, in TypeScript:
class Foo {
bar: number; // no default
} If you do want the class field's default value to be |
err... yeah, that seems to work. which I think solves our problem? we'd just need to update this... svelte/packages/svelte/src/ambient.d.ts Lines 31 to 32 in 00e8ebd
...to this: declare function $state<T>(initial?: T): T; Any reason not to do that? |
Because it doesn't help with this issue, and also introduces risk for other runtime bugs: declare function $state<T>(initial?: T): T;
class X {
notInitialized: string;
notActuallyInitializedButTSStaysSilent: string = $state();
foo() {
this.notActuallyInitializedButTSStaysSilent.startsWith('x'); // oops, runtime error
}
}
function foo() {
let notActuallyInitializedButTSStaysSilent: string = $state();
notActuallyInitializedButTSStaysSilent.startsWith('x'); // oops, runtime error
} |
Yeah... I guess if we were to go that direction, we'd need a way to strip empty |
Not sure if feasible but I think something like this could be a solution: class C {
notInitialized: $state<number> # ts error...
constructor() {
}
} This doesn't require stripping away something before TS sees it, just define |
Hello. I'm surprised that what I do is not listed here. I'll add it to the list hacks built up so far, but as with others, I'd prefer an actual solution. class A {
count = $state<number>() as number; // Now the | undefined part is gone.
constructor(initial: number) {
count = initial;
}
} If you were to ask me, I'd vote for having Svelte allow the use of P.S.: Also allow it in return statements? It would be nice. |
Similarly, I assumed there would be a way to do this via an explicit type: class A {
count: $State<number>
constructor(initial: number) {
count = initial;
}
} |
Let it be noted that @webJose's solution does work, but it disables TypeScript's definite-assignment-in-constructor checks, which are quite important. |
Describe the bug
Consider the following example:
In with a Svelte class, there is no way I can find to declare class fields in this way:
Doesn't work, because
$state
can't be used in the constructor.This seems like a bit of a critical issue for typing classes; there's no way I can find to create a class state property that's assigned in the constructor without adding
undefined
to its type.Reproduction
See description.
Logs
No response
System Info
Severity
annoyance
The text was updated successfully, but these errors were encountered: