Composable, value-backed accessors

A proposal for Stage 1 (Jan 2026 plenary)

Problem space

Class fields introspection recap

leaverou/proposal-class-fields-introspection
- Proposed in Nov 2025 plenary - No consensus to expose existing public class fields - General consensus in the room that a way is needed for classes to be able to declare public data properties **explicitly**

First-class protocols need this too

### Class fields cannot satisfy protocol requirements That would expose class fields to the outside world ([#58](https://github.com/tc39/proposal-first-class-protocols/issues/58))
### Protocols cannot provide class fields If they could, what is `this` in initializers? - If the implementing class, that allows constructor hooks (consensus we don't want that in [#62](https://github.com/tc39/proposal-first-class-protocols/issues/62)) - And anything else is a serious inconsistency

Accessors to the rescue!

  • Already part of the class shape
  • Already introspectable
  • Often the future of these anyway

We just need to make this:


					class C {
						#foo = 1;
						get foo () {
							return this.#foo;
						}
						set foo (v) {
							this.#foo = v;
						}
					}
				

Have the DX of this:


					class C {
						foo = 1;
					}
				
Ahem, have you heard about the [grouped and auto-accessors proposal](https://github.com/tc39/proposal-grouped-and-auto-accessors)?

Yes, and I’ll get to it soon, please bear with me!

Can we fix accessors while we’re at it?

Simple things and complex things

Making complex things possible

❌ Usability cliffs

Doing a complex thing requires recreating the simple thing from scratch.
Incremental value requires disproportionate additional effort.

Example: <video controls> and custom video controls

✅ Extensible simple things

Functionality can be layered on top of the simple thing.
Incremental value requires proportional additional effort.

Example: `Intl.DateTimeFormat`

Accessors use the first model

Example: Data validation

Use case: Throw if `n` is not a positive number


			class C {
				n = 0;
			}
			

			class C {
				#n = 0;
				get n () {
					return this.#n;
				}
				set n(v) {
					if (!(v >= 0)) {
						throw new TypeError("…");
					}
					this.#n = v;
				}
			}
			

Signal Noise

Problem statement 2
Can we improve signal-to-noise ratio?

Ahem, the [grouped and auto-accessors proposal](https://github.com/tc39/proposal-grouped-and-auto-accessors) already does this and is Stage 1 and part of decorators!

Yes, on that…

Grouped and Auto-accessors

Simple case


			class C {
				#n = 0;
				get n () {
					return this.#n;
				}
				set n(v) {
					this.#n = v;
				}
			}
			

				class C {
					accessor n = 1;
				}
			

✅ Excellent signal-to-noise ratio!

Grouped and Auto-accessors

Data validation


			class C {
				#n = 0;
				get n () {
					return this.#n;
				}
				set n(v) {
					if (!(v >= 0)) {
						throw new TypeError("…");
					}
					this.#n = v;
				}
			}
			

			class C {
				#n = 0;
				accessor n {
					get () {
						return this.#n;
					}
					set (v) {
						if (!(v >= 0)) {
							throw new TypeError("…");
						}
						this.#n = v;
					}
				}
			}
			

Is “accessor” the right framing?

  • For the simple case, accessors are an implementation detail.
  • User intent is not "I want an accessor" unless there is logic.
Accessor is still a somewhat obscure term for many JS authors
- Just like functions are an implementation detail for classes and we didn't design class syntax around them - TAG anecdote

Is “accessor” the right framing?

Current mental model: Accessor flowchart

				class C {
					accessor foo = 1;
				}
			
Trollface

Problem statements

  1. We need a high signal-to-noise ratio way to define the data properties that are part of a class' public API
  2. Additive accessor use cases are common enough to deserve better DX
  3. There are good reasons to solve these problems together

Why solve them together?

  1. Public data properties often evolve into additive accessors
  2. Solving them together productively constrains the solution space and prevents language clutter
  3. For this to work, it needs to become universal. For it to become universal, it needs to provide enough value.

Syntax exploration

Please hold off on syntax bikeshedding!

Any and all syntax shown is meant to be illustrative

Composable accessor components*

*not all necessarily in scope

Layers

0 or more

Data validation Should the write proceed?
Data normalization What should be stored?
Side effects Before or after writes
Data transformation What should be read?
Access control Allow reads but not writes

Base

Exactly one

Internal property Never accessed outside the accessor
Existing property Also accessed independently

Composable accessor components

Useful independently, more powerful together

Prior art Swift property observers


			class StepCounter {
				var totalSteps: Int = 0 {
					willSet(newTotalSteps) {
						print("About to set totalSteps to \(newTotalSteps)")
					}
					didSet {
						if totalSteps > oldValue {
							print("Added \(totalSteps - oldValue) new steps")
						}
					}
				}
			}
		

h/tThanks HE Shi-Jun for the example!

- `willSet` cannot reject or transform writes - Implicit `oldValue` in `didSet`

Internal property

Never accessed outside the accessor


					class C {
						#foo = 1;
						get foo () { return this.#foo; }
						set foo (v) { this.#foo = v; }
					}
				

					class C {
						_foo = 1;
						get foo () { return this._foo; }
						set foo (v) { this._foo = v; }
					}
				

					class C {
						[foo] = 1; // symbol
						get foo () { return this[foo]; }
						set foo (v) { this[foo] = v; }
					}
				

				class C {
					propertydata foo = 1;
				}
			

Existing property

Also accessed independently

					class C {
						_foo = 1;
						get foo () {
							return this._foo;
						}
						set foo (v) {
							this._foo = v;
						}
					}
				

					class C {
						_foo = 1;
						aliasforwardaliasproxydelegate foo = _foo;
					}
				

					class C {
						#i = this.attachInternals();
						get foo () {
							return this.#i.foo;
						}
						set foo (v) {
							this.#i.foo = v;
						}
					}
				

					class C {
						#i = this.attachInternals();
						aliasforwardproxydelegate foo = #i.foo;
					}
				

					class C implements P{
						get foo () {
							return this[P.foo];
						}
						set foo (v) {
							this[P.foo] = v;
						}
					}
				

					class C implements P {
						aliasforwardproxydelegate foo = P.foo;
					}
				

Syntax for layers? (old)

Option 1 New MethodDefinition keywords & descriptor keys Option 2Built-in decorators
Example

							property n = 0;
							validate n (v) { return v >= 0; }
						

							@validate(v => v >= 0)
							property n = 0;
						
Scope More substantial change Smaller delta
Readability Important bits first Auxiliary bits first
Lossiness Remain separate from `set` → lossless.
Can even be decorated separately!
Sugar that wraps `set` → lossy
Imperative API `Object.defineProperty()` None (by design)
Object literal support? Out of the box [Speculative future extension](https://github.com/tc39/proposal-decorators/blob/master/EXTENSIONS.md)
Composeability Could compose with any member Can only be applied to accessors
Technically another option would be to have the same `validate()` syntax that is just sugar, but then there is no benefit over a (built-in) decorator

Additional advantages of using built-in decorators

Auto-upgrade regular data properties?


				let obj = {
					_n: 0,
					get n () {
						return this._n;
					},
					set n (v) {
						if (v >= 0) {
							this._n = v;
						}
					}
				};
			

				let obj = {
					property n: 0,
					validate n (v) {
						return v >= 0;
					}
				};
			

				let obj = {
					n: 0,
					validate n (v) {
						return v >= 0;
					}
				};
			

Auto-upgrade regular data properties?


			let obj = {
				get n () {
					return this._n;
				},
				set n (v) {
					if (v >= 0) {
						this._n = v;
					}
				}
			};
		

			let obj = {
				property n,
				validate n (v) {
					return v >= 0;
				}
			};
		

			let obj = {
				validate n (v) {
					return v >= 0;
				}
			};
		

With grouped accessors


				class C {
					property n = 0;
					validate n (v) {
						return v >= 0;
					}
					normalize n (v) {
						return Number(v);
					}
				}
			

			class C {
				property n {
					validate (v) {
						return v >= 0;
					}
					normalize (v) {
						return Number(v);
					}
				} = 0;
			}
		

Problem statements

Second session

Recap of previous discussion

Syntax for layers?

Direction Option 1 New MethodDefinition keywords & descriptor keys Option 2Built-in decorators
Example

							property n = 0;
							validate n (v) { return v >= 0; }
						

							@validate(v => v >= 0)
							property n = 0;
						
Scope Much substantial change Smaller delta
Readability Important bits first Auxiliary bits first. Mitigated through references for longer functions.
Lossiness Remain separate from `set` → lossless.
Can even be decorated separately!
Sugar that wraps set → lossy. Can be designed to preserve the original setter somewhere.
Imperative API `Object.defineProperty()` None (by design). Can be mitigated via object literal decorators.
Technically another option would be to have the same `validate()` syntax that is just sugar, but then there is no benefit over a (built-in) decorator

Additional advantages of using built-in decorators

During the break…

I'm looking at my accessors usage, and they are (numbers are not measured): - 75% "property forwarding" - 15% lazy initial computation - 10% validation - 5% other — Nicolò Ribaudo

Changes before moving to tc39/

  1. Remove value-backed accessors and work with Ron Buckton to explore potential improvements to auto-accessors instead.
  2. Focus this proposal on composable accessors and split into two proposals:
    1. Composable accessors via built-in decorators
    2. Alias accessors

Composable accessors via built-in decorators

Problem statement: There are large classes of accessor use cases with strong commonalities and they deserve better DX and tooling support.

This proposal aims to explore which of these may have good Impact/Effort to expose as built-in decorators.


				import isFoo, hasBar from "./validators.js";
				const { validate, lazy } = SomeObject;

				class C {
					// Signature tentative, do not 🚲shed!
					@validate(isFoo, hasBar) accessor foo;

					@lazy accessor foo {
						get () {
							return expensiveComputation();
						}
					}
				}
			

Alias accessors

Problem statement: Alias accessors are a particularly large class of accessor use cases and may be worth exploring separately, as they are more likely to need syntax for reasonable DX and to access privates.

  • Could eventually be merged into the auto-accessors / grouped accessors proposal (see #14)
  • Ron had some implementation / speccing concerns but they were non-blocking for Stage 1

				class C {
					// Indicative syntax, do not bikeshed
					alias foo = #foo.value;

					alias bar = #internals.bar {
						get;
						#set;
					}
				}