- Notifications
You must be signed in to change notification settings - Fork13.2k
Implement the Stage 3 Decorators Proposal#50820
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Conversation
fatcerberus commentedSep 18, 2022 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
At some point this flag should probably be aliased/renamed to the thought process I’m imagining is essentially, “ooh, I like ES decorators, I wonder if this will give me even cooler decorator features…” |
rbuckton commentedSep 19, 2022
Maybe aliased, but probably not renamed so as not to break existing consumers. Also, parameter decorators are still experimental. |
fatcerberus commentedSep 19, 2022 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Yeah, my point was more that at some point we’re going to have a flag called “experimental” that opts intolegacy behavior, and worse, legacy behavior that’s incompatible with the standard behavior that’ll be supported by default. It’s a weird state of affairs and I can definitely foresee the future GH issues “I enabled experimentalDecorators and all my existing decorators stopped working correctly, I thought this would just unlock additional features” |
a3ec9ee toe87b89cCompare2d7e2b2 to8c33a18Compareruojianll commentedSep 25, 2022 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I have 2 questions:
|
rbuckton commentedSep 25, 2022
That is the current behavior of the proposal, but an alternative is being discussed intc39/proposal-decorators#465.
Class decorators can only return functions. You are welcome to open an issue athttps://github.com/tc39/proposal-decorators if you believe this should be changed. |
831a5b2 to673073cComparerobpalme commentedJan 20, 2023
The ES2022 downlevel wraps the class in a function in order to provide encapsulated access to a couple of private static bindings ( classMyClass{ @MyDecoratorstaticmethod(){}}; ...is downlevelled to ES2022 like this... letMyClass=(()=>{let_staticExtraInitializers=[];let_static_method_decorators;returnclassMyClass{static{_static_method_decorators=[MyDecorator];__esDecorate(this,null,_static_method_decorators,{kind:"method",name:"method",static:true,private:false},null,_staticExtraInitializers);__runInitializers(this,_staticExtraInitializers);}staticmethod(){}};})(); Would it be possible/desirable to specialize the emit for ES2022+ to use class private bindings? That would eliminate the function-wrapping and slightly improve the debugging experience (smaller callstack + in the object inspector # privates have less noise than closures). classMyClass{static #staticExtraInitializers=[];static #method_decorators;static{this.#static_method_decorators=[MyDecorator];__esDecorate(this,null,this.#static_method_decorators,{kind:"method",name:"method",static:true,private:false},null,this.#staticExtraInitializers);__runInitializers(this,this.#staticExtraInitializers);}staticmethod(){}}; |
rbuckton commentedJan 20, 2023
I considered this early on. Except for instance "initializers", those private fields would be unused after class definition evaluation and would take up extra space on the class itself. In addition, those temporary values can't be garbage collected. I'd argue they also make debugging worse when interrogating a Watch window given the excess properties attached to the class. |
pflannery commentedJan 24, 2023
I just tried this with
I have tests that use a title decorator to override class names for test suite naming. // foo.tests.js@testTitle("foo bar")exportclassFooTests{// test functions...} Is this a bug or is there an option to allow this syntax for js?
Does this mean typescript files support this syntax? |
jakebailey commentedJan 24, 2023
My understanding of the spec is that the above has to be written: export@testTitle("foo bar")classFooTests{// test functions...} So, we emit an error for the other form. But I believe for convenience, you can write it the "old way" in TypeScript code and it will emit it the other way around. |
rbuckton commentedJan 24, 2023
In a We still think that placing decoratorsafter |
robpalme commentedJan 25, 2023
I won't argue for using class privates in the implementation. There's just one bit of clarification.
In practice the storage costs and lifetimes are equivalent between class privates and closures. Due to a long-standing implementation detail in the way closures work in pretty much all engines, the lifetime of captured bindings matches the outer closure despite the fact that inner functions no longer have need for them. So with the current Decorators implementation, those temporary values (e.g. the So if we want to release that memory earlier, we need to release it manually by setting it to |
asnaeb commentedJan 25, 2023 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I noticed that with the nightly builds, declarefunctionstringField(_:undefined,ctx:ClassFieldDecoratorContext<A,string>):void|((this:A,value:string)=>string)declarefunctionstringFieldVoid(_:undefined,ctx:ClassFieldDecoratorContext<A,string>):voiddeclareclassA{ @stringFieldfoo:number @stringFieldVoid// <- used to error, now doesn't anymorebar:number} The same code with a previous version of this PR: I did not test if this occurs with other |
DanielRosenwasser commentedJan 25, 2023
Oh no - the removal of |
asnaeb commentedJan 25, 2023
I have updated the comment. Declaring a full return type seems to fix this issue |
rbuckton commentedJan 25, 2023
I'd hate to remove the type param. The only other option I can think of would be to reintroduce |
asnaeb commentedJan 25, 2023 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Using the return type instead of declarefunctionobjectField<TextendsRecord<string,any>>(_:undefined,ctx:ClassFieldDecoratorContext<A>):void|((value:T)=>T)declareclassA{ @objectFieldfoo:{a:string// <- Non-optional fields are not allowed here.b?:{c:number}} @objectFieldbar:{a?:stringb?:{c:number// <- Non-optional fields are allowed here.}}} To me, this looks inconsistent:Nightly Playground Link This was not the case when using the type args on |
DanielRosenwasser commentedJan 25, 2023
We could declare it as interfaceClassFieldDecoratorContext<This=unknown,inoutValue=unknown> Though I think originally, |
asnaeb commentedJan 31, 2023 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
should we keep watching here for updates on this? |
DanielRosenwasser commentedFeb 1, 2023 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I'd prefer people not continue discussing on the PR. The conversation is already very long, and it is hard to keep track of new issues. I opened up#52540 to track the last-minute Any other questions/comments should be filed as new issues. Thanks all! |

Uh oh!
There was an error while loading.Please reload this page.
This implements support for the Stage 3 Decorators proposal targeting
ESNextthroughES5(except where it depends on functionality not available in a specific target, such as WeakMaps for down-level private names).The following items arenot currently supported:
--emitDecoratorMetadata, as metadata is currently under discussion inhttps://github.com/tc39/proposal-decorator-metadata and has not yet reached Stage 3.declarefield.With that out of the way, the following items are whatis supported, or is new or changed for Decorators support in the Stage 3 proposal:
--experimentalDecoratorsflag will continue to opt-in to the legacy decorator support (which still continues to support--emitDecoratorMetadataand parameter decorators).--experimentalDecoratorsflag.ESNext(or at least, until such time as the proposal reaches Stage 4).targetandcontext:target— A value representing the element being decorated:getaccessors, andsetaccessors: This will be the function for that element.accessor x): This will be an object withgetandsetproperties.undefined.context— An object containing additional context information about the decorated element such as:kind- The kind of element ("class","method","getter","setter","field","accessor").name- The name of the element (either astringorsymbol).private- Whether the element has a private name.static- Whether the element was declaredstatic.access- An object with either agetproperty, asetproperty, or both, that is used to read and write to the underlying value on an object.addInitializer- A function that can be called to register a callback that is evaluated either when the class is defined or when an instance is created:getandsetdeclarations)no longer receive the combined property descriptor. Instead, they receive the accessorfunction they decorate.get/setpairs can be found athttps://github.com/tc39/proposal-grouped-and-auto-accessors.staticmember, you can use:enumerable,configurable, orwritableproperties as they do not receive the property descriptor. You can partially achieve this viacontext.addInitializer, but with the caveat that initializers added by non-static member decorators will run duringevery instance construction.This is not currently consistent in all cases and is only set when transforming native ES Decorators or class fields. While we generally have not strictly aligned with the ECMA-262 spec with respect to assigned names when downleveling classes and functions (sometimes your class will end up with an assigned name of
class_1ordefault_1), I opted to include this becausenameis one of the few keys available to a class decorator's context object, making it more important to support correctly.Type Checking
When a decorator is applied to a class or class member, we check that the decorator can be invoked with the appropriatetarget anddecorator context, and that its return value is consistent with its target. To do this, we check the decorator against a synthetic call signature, not unlike the following:
The types we use for
T,C, andRdepend on the target of the decorator:T— The type for the decorationtarget. This does not always correspond to the type of a member.{ get, set }object corresponding to the generatedget method andset method signatures.undefined.C— The type for thedecorator context. A context type based on the kind of decoration type, intersected with an object type consisting of the target'sname,placement, andvisibility (see below).R— The allowed type for the decorator's return value. Note that any decorator may returnvoid/undefined.T.{ get?, set?, init? }whosegetandsetcorrespond to the generatedget method andset method signatures. The optionalinitmember can be used toinject aninitializer mutator function.
Method Decorators
Amethod decorator applied to
m(): voidwould use the typesresulting in a call signature like
Here, we specify atarget type (
T) of(this: MyClass) => void. We don't normally traffic around thethistype for methods, but in this case it is important that we do. When a decoratorreplaces a method, it is fairly common to invoke the method you are replacing:You may also notice that we intersect a common context type, in this case
ClassMethodDecoratorContext, with a type literal. This type literal contains information specific to the member, allowing you to write decorators that are restricted to members with a certain name, placement, or accessibility. For example, you may have a decorator that is intended to only be used on theSymbol.iteratormethod, or one that is restricted to
staticfields, or one that prohibits usage on private members
We've chosen to perform an intersection here rather than add additional type parameters to each*DecoratorContext type for several reasons. The type literal allows for a convenient way to introduce a restriction in your decorator code without needing to fuss over type parameter order. Additionally, in the future we may opt to allow a decorator to replace thetype of its decoration target. This means we may need to flow additional type information into the context to support the
accessproperty, which acts on thefinal type of the decorated element. The type literal allows us to be flexible with future changes.Getter and Setter Decorators
Agetter decorator applied to
get x(): stringabove would have the typesresulting in a call signature like
, while asetter decorator applied to
set x(value: string)would have the typesresulting in a call signature like
Getter andsetter decorators in the Stage 3 decorators proposal differ significantly from TypeScript's legacy decorators. Legacy decorators operated on a
PropertyDescriptor, giving you access to both thegetandsetfunctions as properties of the descriptor. Stage 3 decorators, however, operate directly on thegetandsetmethods themselves.Field Decorators
Afield decorator applied to a field like
#x: stringabove (i.e., one that does not have a leadingaccessorkeyword) would have the typesresulting in a call signature like
The
targetof afield decorator is alwaysundefined, as there is nothing installed on the class or prototype during declaration evaluation. Non-staticfields are installed only when an instance is created, whilestaticfields are installed only after all decorators have been evaluated. This means that you cannot replace a field in the same way that you can replace a method or accessor. Instead, you can return aninitializer mutator function — a callback that can observe, and potentially replace, the field's initialized value prior to the field being defined on the object:This essentially behaves as if the following happened instead:
Auto-Accessor Decorators
Stage 3 decorators introduced a new class element known as an "Auto-Accessor Field". This is a field that is transposed into pair of
get/setmethods of the same name, backed by a private field. This is not only a convenient way to represent a simple accessor pair, but also helps to avoid issus that occur if a decorator author were to attempt to replace an instance field with an accessor on the prototype, since an ECMAScript instance field would shadow the accessor when it is installed on the instance.Anauto-accessor decorator applied to a field like
accessor y: stringabove would have the typesresulting in a call signature like
Note that
Tin the example above is essentially the same as, while
Ris essentially the same asThe return value (
R) is designed to permit replacement of thegetandsetmethods, as well as injecting aninitializer mutator function like you can with a field.Class Decorators
Aclass decorator applied to
class MyClasswould use the typesresulting in a call signature like
Fixes#48885