Skip to content

tc39/proposal-alias-accessors

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 

Alias Accessors

Status

Stage: 1 (as of the Jan 2026 plenary)

Author/champion: Lea Verou (@leaverou)

For history, see the original proposal.

Contents

  1. Status
  2. Motivation
    1. Why not just use decorators?
  3. Design
    1. Aggregation
    2. Read-only aliases
    3. Integration with grouped and auto-accessors

Motivation

The vast majority (possibly over 90%, though it's hard to prove) of accessor use cases are additive. Their conceptual model is not entirely arbitrary logic, but a layering of transformations, side effects, and access control over a regular property (public or private).

In some cases, this property is an internal implementation detail, and is never accessed separately. These cases are well served by auto-accessors together with built-in decorators.

However, when a pre-existing property (often deeply nested) is used as the backing store, authors need to fall back to the usual boilerplate.

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

This proposal explores potential syntaxes with higher signal-to-noise ratio for these use cases, such as:

class C {
	#foo = new Signal(1);
	alias foo to #foo.value;
}

Some (not mutually exclusive) high-level use cases for this are:

  • Encapsulation: obscure the data source so it can be changed later
  • Ergonomics: shorten frequently accessed property chains
  • Composition: Selectively expose API surface from other objects or first-class protocols
  • Access control: private properties with public getters

Anecdotally:

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 (@nicolo-ribaudo)

Why not just use decorators?

Private fields as targets is a very important use case, and there is currently no way to pass them around separately from the object they are defined on.

But even for public fields, a decorator would be pretty awkward:

class C {
	// But we're not setting this to an array!
	@alias accessor foo = ["a", "b", "c"];
}
class C {
	// Better, but still confusing
	@alias(["a", "b", "c"]) accessor foo;
}

Perhaps with something like reference declarations or similar it may become palatable:

class C {
	@alias accessor foo = x => ref x.a.#b.c;
}

or

class C {
	@alias(x => ref x.a.#b.c) accessor foo;
}

But it's still far from ideal.

Design

Just like auto-accessors, if no side effects or logic is desired, proxying another property should involve little additional syntax over the property name and a reference to the property (chain) holding the underlying value. It could even be a separate type of value-backed accessor, taking a property reference instead of an initial value, e.g.:

class C {
	somekeyword foo someseparator #foo.value;
}

This would be sugar for:

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

So the syntax to be bikeshedded is:

  1. Keyword to prepend the definition. Ideas:
    • alias
    • delegate
    • forward
    • derived
    • bind Could be confused with the bind method
    • proxy: Could be confused with the Proxy constructor
  2. Separator between the property name and the property chain being proxied. Ideas:
    • =: Could be confused with the assignment operator
    • =>: Could be confused with the arrow function operator, but also indicates a binding
    • via
    • from
    • through
    • to

For concreteness, we’ll use alias/to in the rest of this document.

These property chains are basically chains of [ . LiteralPropertyName ] | ComputedPropertyName ]. this. at the start is implicit.

They are not References however: conceptually, the entire chain is re-evaluated on each access.

Aggregation

There are many cases where multiple properties need to be forwarded through another object. For example, when using delegation/forwarding patterns, such as ElementInternals, multiple properties need to be exposed.

Current syntaxPotential new syntax
class MyElement extends HTMLElement {
	#internals = this.attachInternals();
	static formAssociated = true;

	// Expose
	get form () {
		return this.#internals.form;
	}
	get labels () {
		return this.#internals.labels;
	}
	get checkValidity () {
		return this.#internals.checkValidity;
	}
	get reportValidity () {
		return this.#internals.reportValidity;
	}
	get willValidate () {
		return this.#internals.willValidate;
	}
	get validationMessage () {
		return this.#internals.validationMessage;
	}
	// ...
}
class MyElement extends HTMLElement {
	#internals = this.attachInternals();
	static formAssociated = true;

	// Expose
	alias form              = #internals.form;
	alias labels            = #internals.labels;
	alias checkValidity     = #internals.checkValidity;
	alias reportValidity    = #internals.reportValidity;
	alias willValidate      = #internals.willValidate;
	alias validationMessage = #internals.validationMessage;
	// ...
}

or when implementing a first-class protocol:

Current syntaxPotential new syntax
class C implements Iterable {
	get forEach () {
		return this[Iterable.forEach];
	}
	get map () {
		return this[Iterable.map];
	}
	get flatMap () {
		return this[Iterable.flatMap];
	}
	get filter () {
		return this[Iterable.filter];
	}
	get reduce () {
		return this[Iterable.reduce];
	}
	get toArray () {
		return this[Iterable.toArray];
	}
	get some () {
		return this[Iterable.some];
	}
	get every () {
		return this[Iterable.every];
	}
	get find () {
		return this[Iterable.find];
	}
	static get from () {
		return this[Iterable.from];
	}
	// ...
}
class C implements Iterable {
	alias forEach = [Iterable.forEach];
	alias map     = [Iterable.map];
	alias flatMap = [Iterable.flatMap];
	alias filter  = [Iterable.filter];
	alias reduce  = [Iterable.reduce];
	alias toArray = [Iterable.toArray];
	alias some    = [Iterable.some];
	alias every   = [Iterable.every];
	alias find    = [Iterable.find];
	alias from    = [Iterable.from];
	// ...
}

While alias improves things quite a lot already, it’s still a little repetitive. There is not much to do in the second case, but perhaps there could be a destructuring-inspired shortcut when the names are identical:

class C {
	alias {
		form,
		labels,
		checkValidity,
		reportValidity,
		willValidate,
		validationMessage
	} from #internals;
}

Read-only aliases

While by default both a setter and a getter would be added, composable setters could be used to make it read-only, either with silent rejection (DOM style) or loud rejection (JS style):

class C {
	#foo = 1;
	@validate(v => false)
	alias #foo to foo;
}

Do note however that this is only needed if the property being proxied is read-only, the error would just propagate naturally:

class MyElement extends HTMLElement {
	#internals = this.attachInternals();

	// ElementInternals.prototype.form is read-only
	// so setting form will throw
	alias form = #internals.form;
}

Here, setting myElement.form would produce an error anyway, since ElementInternals.prototype.form is read-only. Therefore, there is no need to prevent writes explicitly unless we want to swallow them.

Integration with grouped and auto-accessors

Another direction for this proposal might be to become a syntax extension of grouped and auto-accessors. Customization of the backing store was already requested in #14.

The syntax proposed in the issue was accessor(#x) x = 1;. Syntax like accessor x : #x = 1 would likely clash with TS.

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks