-
Notifications
You must be signed in to change notification settings - Fork 106
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
confusing that extra initializers for auto accessors and regular accessors do not both run after storage initializers. #548
Comments
This is admittedly a bit of a confusing ordering. There are a few reasons for the timing differences here altogether, we went through several iterations to get here, so I'll take us through it again to document the reasons.
So that's why we're here. Now, a few thoughts on where we could go from here:
function attribute(_, context) {
const {name, kind, access, metadata} = context
if (kind === 'accessor' || kind === 'getter') {
context.addInitializer(function () {
context.metadata['...'] = () => access.call(this);
});
}
} This would make the access call to the value lazy, which would prevent it from running until after all of the initialization code had completed. But, you wouldn't be able to cache the initial value of that property in this way.
function attribute(value, context) {
const {name, kind, access, metadata} = context
if (kind === 'accessor' || kind === 'setter') {
return {
set() {
context.metadata['...'] = { dirtied: true }
}
}
}
} |
Note
Keep in mind that this is feedback coming in after using decorators, which was not previously possible until tools like TypeScript and Babel have more recently implemented the Stage 3 spec and we've had time to work with them in the field.
Fundamentally speaking, the new
accessor
keyword is basically syntax sugar forget
/set
members with private storage. However, when decorating them, the behavior is not the same, which can result in not being able to achieve the same result as we can do withaccessor
usingset
/get
, therefore eliminating some of the sugary aspect ofaccessor
.For example, intuitively these would behave the same (but they don't, so there's a runtime error):
TypeScript playground, Babel Repl
The fact that we cannot access the
get
ter/set
ter in an initializer is problematic, requiring the use of a class decorator if we need the initial value (and this eliminates the getter/setter from being reliable in a user's ownconstructor
).Use case
When implementing an
@attribute
decorator that maps Custom Element attributes to JS properties, I want to set the initial value of a JS property back to the storage's initial value when the associated HTML attribute is removed.With accessors, this is easy:
(See
@lume/element
for a real implementation)One would think they could simply do the same thing with a
get
/set
member:But that won't work because any field (private or not) that the getter is using for storage will not be ready yet! With public fields, this can be worked around by returning
this.__notPrivateStorage ?? theDefaultValue
from the getter. But with private fields, a runtime error will happen.Workaround 1
One way to make it work then, is to require the class decorator to handle the initial value:
Workaround 2
Another way to solve the issue is to provide a special decorator to use on the getter/setter storage in tandem with the attribute decorator:
The implementation is omitted, but basically it will track the initial value in the user constructor, rather than in a class decorator's subclass.
Workaround 3
Another way to solve it is to provide a finalizer decorator to use on a dummy field that can handle initial values:
The implementation is omitted, but the
@attribute.finalize
decorator will run after all fields, which means it also runs after all get/set extra initializers, and hence it will handle initial values in the user constructor instead of in a class decorator's subclass.Workaround 4
The least ideal solution imo (because it causes user-specific detail to be duplicated) is to have the user provide the initial value to the decorator:
As you can see, now the values are duplicated.
TLDR
If the initializers ran at the same time, all of the workarounds could be avoided, complexity reduced.
If running the extra initializers after storage initializers for get/set members adds other complexity to other use cases unlike the one I described above, then I think it is imperative that we allow defining which types of initializers we wish to add (extra initializers that run before storage initializers, or extra initializers that run after storage initializers).
Related conversations:
context.addPostInitializer()
#521@rbuckton's "bucket" idea defines an enum of values to pass to
addInitializer
to specify where we want a initializer to happen:The text was updated successfully, but these errors were encountered: