Skip to content

Commit 9bd3e42

Browse files
committed
feat(svelte): split Internal class into read/write helpers
The svelteState function now branches on arguments.length to instantiate either InternalRead or InternalWrite. This makes the generated code clearer and satisfies a developer request while preserving correct typing when writing undefined. Added a new unit test covering explicit-undefined write behavior.
1 parent cc7a117 commit 9bd3e42

File tree

2 files changed

+44
-12
lines changed

2 files changed

+44
-12
lines changed

src/svelte.svelte.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,40 @@ export function svelteState<T>(props?: T): T | undefined {
3535
// constructor. To work around that limitation we embed the store access
3636
// in a small helper class that performs the call from a constructor.
3737
//
38-
// Each invocation of `svelteState` creates a fresh instance of
39-
// `Internal` so the generic typing is preserved, and the actual call to
40-
// `$state` happens in the constructor where the compiler is happy.
41-
class Internal<U> {
42-
readonly val?: U;
43-
44-
constructor(v?: U) {
45-
// first assignment to a class field in the constructor – permitted by the compiler
46-
const val = $state(v); // wtf: <https://github.com/sveltejs/svelte/issues/14600#issuecomment-2528564271>
47-
this.val = val;
38+
// Earlier versions used a single `Internal` class for both read and write
39+
// operations. That worked fine, but the compiler only knows whether we're
40+
// using `new Internal(...)` with or without an argument at compile time. To
41+
// make the generated code more explicit (and satisfy a developer request) we
42+
// now branch at runtime and define *two* tiny helper classes. One is used
43+
// when the caller omitted arguments (read operation) and simply calls
44+
// `$state()`; the other is used when a value is provided and forwards that
45+
// value to `$state(v)`.
46+
//
47+
// We detect the difference using `arguments.length` rather than checking
48+
// `props === undefined`, because the generic `T` may legitimately be
49+
// `undefined` when the caller intends to store that value.
50+
if (arguments.length === 0) {
51+
class InternalRead<U> {
52+
readonly val: U | undefined;
53+
constructor() {
54+
// call with no arguments – allowed by the compiler inside the ctor
55+
const val = $state<U>();
56+
this.val = val;
57+
}
4858
}
59+
return new InternalRead<T>().val;
60+
} else {
61+
// a value must exist because `arguments.length > 0`, but the generic type
62+
// may legitimately be `undefined`. Capture it in a local variable and
63+
// use a simple type assertion (not a forbidden non-null assertion comment)
64+
const value = props as T;
65+
class InternalWrite<U> {
66+
readonly val: U;
67+
constructor(v: U) {
68+
const val = $state(v);
69+
this.val = val;
70+
}
71+
}
72+
return new InternalWrite<T>(value).val;
4973
}
50-
51-
return new Internal<T>(props).val;
5274
}

tests/src/svelte.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,14 @@ describe("svelteState helper", () => {
4343
expect(mockState).toHaveBeenCalledWith();
4444
expect(result).toBe(123);
4545
});
46+
47+
it("distinguishes an explicit undefined write from a read", () => {
48+
// when `undefined` is intentionally stored we still expect `$state` to be
49+
// invoked with that value rather than being called with no arguments.
50+
mockState.mockReturnValue("ok");
51+
52+
const result = svelteState<undefined>(undefined);
53+
expect(mockState).toHaveBeenCalledWith(undefined);
54+
expect(result).toBe("ok");
55+
});
4656
});

0 commit comments

Comments
 (0)