Repository: sveltejs/svelte (2025-07-25T18:56:23Z)

Issue:
Writing getter for public class stateful variables transforms into duplicate getters
### Describe the bug

`$state()` can be used to define reactive class instance variables. This is generally very useful, though when writing a getter for public variables, there's a call stack error.

I later discovered this is called out in the docs:

> The compiler transforms `done` and `text` into `get` / `set` methods on the class prototype referencing private fields. This means the properties are not enumerable.
> — https://svelte.dev/docs/svelte/$state#Classes

I see from the JS output that a compiled `$.get(this.#myvar)` is necessary when returning the variable. Could the compiler detect when manually written getters are made, and perform this code transformation on our behalf?

Or instead, provide a warning when a `get myvar() { return this.myvar }` is written by the author, and provide education on the private variable getter/setter transformation.

### Reproduction

1. Go to https://svelte.dev/playground/5eb453986f3f4615a312342b59bd297c?version=5.36.16
2. Open `store.svelte.js`
3. Observe transformed store has 2 getters

### System Info

```shell
Svelte 5.36.16
```

### Severity

annoyance


Discussion:
~~Isn't it the code wrong from the very beginning? Class cannot have a field and getter/setter with the same name - they conflict.~~
Edit: How this black magic works and does not confuse anybody?
Edit2: So, the getter goes to the prototype, while the field is on the instance and shadows the getter. So, I still think the code is "wrong" to begin with and this approach doesn't make sense.
> So, I still think the code is "wrong" to begin with and this approach doesn't make sense.

I think I agree with you. It turns out I misunderstood the class concept. For example:

```js
class Example {
  name = 'Person'

  get name() { return 'Hi ' + this.name }
}

const test = new Example()
test.name // yields 'Person', and not 'Hi Person'
```

Writing a getter for a public class variable does not work the way I expected since it's already duplicative. We can close this issue or perhaps show a warning to the developer when they attempt this; the argument for providing a warning is doing this pattern in Svelte would cause a "call stack size exceeded" error whereas this would fall back to returning the instance variable in plain JS, and there's an opportunity to link to the docs. I'm happy with either outcome.



Patch:
diff --git a/.changeset/odd-phones-taste.md b/.changeset/odd-phones-taste.md
new file mode 100644
index 000000000000..ec9534b74157
--- /dev/null
+++ b/.changeset/odd-phones-taste.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: throw on duplicate class field declarations
diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md
index 20f57770d122..957a9f67c7b0 100644
--- a/documentation/docs/98-reference/.generated/compile-errors.md
+++ b/documentation/docs/98-reference/.generated/compile-errors.md
@@ -364,6 +364,12 @@ The $ name is reserved, and cannot be used for variables and imports
 The $ prefix is reserved, and cannot be used for variables and imports
 ```
 
+### duplicate_class_field
+
+```
+`%name%` has already been declared
+```
+
 ### each_item_invalid_assignment
 
 ```
diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md
index 2b0c5eafdf86..5c1080acedfe 100644
--- a/packages/svelte/messages/compile-errors/script.md
+++ b/packages/svelte/messages/compile-errors/script.md
@@ -30,6 +30,10 @@
 
 > The $ prefix is reserved, and cannot be used for variables and imports
 
+## duplicate_class_field
+
+> `%name%` has already been declared
+
 ## each_item_invalid_assignment
 
 > Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`)
diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js
index 599d3e8248b0..e763a6e0733a 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -152,6 +152,16 @@ export function dollar_prefix_invalid(node) {
 	e(node, 'dollar_prefix_invalid', `The $ prefix is reserved, and cannot be used for variables and imports\nhttps://svelte.dev/e/dollar_prefix_invalid`);
 }
 
+/**
+ * `%name%` has already been declared
+ * @param {null | number | NodeLike} node
+ * @param {string} name
+ * @returns {never}
+ */
+export function duplicate_class_field(node, name) {
+	e(node, 'duplicate_class_field', `\`${name}\` has already been declared\nhttps://svelte.dev/e/duplicate_class_field`);
+}
+
 /**
  * Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`)
  * @param {null | number | NodeLike} node
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
index ffc39ac00de1..2bfc1dbce36c 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
@@ -33,6 +33,9 @@ export function ClassBody(node, context) {
 	/** @type {Map<string, StateField>} */
 	const state_fields = new Map();
 
+	/** @type {Map<string, Array<MethodDefinition['kind'] | 'prop' | 'assigned_prop'>>} */
+	const fields = new Map();
+
 	context.state.analysis.classes.set(node, state_fields);
 
 	/** @type {MethodDefinition | null} */
@@ -54,6 +57,14 @@ export function ClassBody(node, context) {
 				e.state_field_duplicate(node, name);
 			}
 
+			const _key = (key.type === 'PrivateIdentifier' ? '#' : '') + name;
+			const field = fields.get(_key);
+
+			// if there's already a method or assigned field, error
+			if (field && !(field.length === 1 && field[0] === 'prop')) {
+				e.duplicate_class_field(node, _key);
+			}
+
 			state_fields.set(name, {
 				node,
 				type: rune,
@@ -67,10 +78,48 @@ export function ClassBody(node, context) {
 	for (const child of node.body) {
 		if (child.type === 'PropertyDefinition' && !child.computed && !child.static) {
 			handle(child, child.key, child.value);
+			const key = (child.key.type === 'PrivateIdentifier' ? '#' : '') + get_name(child.key);
+			const field = fields.get(key);
+			if (!field) {
+				fields.set(key, [child.value ? 'assigned_prop' : 'prop']);
+				continue;
+			}
+			e.duplicate_class_field(child, key);
 		}
 
-		if (child.type === 'MethodDefinition' && child.kind === 'constructor') {
-			constructor = child;
+		if (child.type === 'MethodDefinition') {
+			if (child.kind === 'constructor') {
+				constructor = child;
+			} else if (!child.computed) {
+				const key = (child.key.type === 'PrivateIdentifier' ? '#' : '') + get_name(child.key);
+				const field = fields.get(key);
+				if (!field) {
+					fields.set(key, [child.kind]);
+					continue;
+				}
+				if (
+					field.includes(child.kind) ||
+					field.includes('prop') ||
+					field.includes('assigned_prop')
+				) {
+					e.duplicate_class_field(child, key);
+				}
+				if (child.kind === 'get') {
+					if (field.length === 1 && field[0] === 'set') {
+						field.push('get');
+						continue;
+					}
+				} else if (child.kind === 'set') {
+					if (field.length === 1 && field[0] === 'get') {
+						field.push('set');
+						continue;
+					}
+				} else {
+					field.push(child.kind);
+					continue;
+				}
+				e.duplicate_class_field(child, key);
+			}
 		}
 	}
 


Test patch:
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json
index b7dd4c8ed457..94b5f191c2f7 100644
--- a/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json
@@ -1,14 +1,14 @@
 [
 	{
-		"code": "state_field_invalid_assignment",
-		"message": "Cannot assign to a state field before its declaration",
+		"code": "duplicate_class_field",
+		"message": "`count` has already been declared",
 		"start": {
-			"line": 2,
-			"column": 1
+			"line": 5,
+			"column": 2
 		},
 		"end": {
-			"line": 2,
-			"column": 12
+			"line": 5,
+			"column": 24
 		}
 	}
 ]