Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

Merged
rbuckton merged 69 commits intomainfromdecorators-stage-3
Jan 19, 2023
Merged

Conversation

rbuckton
Copy link
Contributor

@rbucktonrbuckton commentedSep 17, 2022
edited
Loading

This implements support for the Stage 3 Decorators proposal targetingESNext throughES5 (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:

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:

  • The--experimentalDecorators flag will continue to opt-in to the legacy decorator support (which still continues to support--emitDecoratorMetadata and parameter decorators).
  • ES Decorators are now supportedwithout the--experimentalDecorators flag.
  • 🆕 ES Decorators will be transformed when the target is less thanESNext (or at least, until such time as the proposal reaches Stage 4).
  • 🆕 ES Decorators now accept exactly two arguments:target andcontext:
    • target — A value representing the element being decorated:
      • Classes, Methods,get accessors, andset accessors: This will be the function for that element.
      • Auto-Accessor fields (i.e.,accessor x): This will be an object withget andset properties.
      • Fields: This will always beundefined.
    • 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 astring orsymbol).
      • private - Whether the element has a private name.
      • static - Whether the element was declaredstatic.
      • access - An object with either aget property, aset property, 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:
        • For static member decorators, initializers run after class decorators have been applied but before static fields are initialized.
        • For Class Decorators, initializers run after all static initializers.
        • For non-static member decorators, initializers run in the constructor before all field initializers are evaluated.
  • 🆕 ES Decorators can decorateprivate fields.
  • 🆕 ES Decorators can decorateclass expressions.
  • ‼️ ES Accessor Decorators (i.e., forget andset declarations)no longer receive the combined property descriptor. Instead, they receive the accessorfunction they decorate.
  • ‼️ ES Member Decorators (i.e., for accessors, fields, and methods)no longer have immediate access to the constructor/prototype the member is defined on.
  • ‼️ ES Member Decorators can no longer set theenumerable,configurable, orwritable properties 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.
  • When the name of the class is inferred from an assignment, we will now explicitly set the name of the classin some cases.
    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 ofclass_1 ordefault_1), I opted to include this becausename is 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:

typeSyntheticDecorator<T,C,R>=(target:T,context:C)=>R|void;

The types we use forT,C, andR depend on the target of the decorator:

  • T — The type for the decorationtarget. This does not always correspond to the type of a member.
    • For a class decorator, this will be the class constructor type.
    • For a method decorator, this will be the function type of the method.
    • For a getter decorator, this will be the function type of theget method,not the type of the resulting property.
    • For a setter decorator, this will be the function type of theset method,not the type of the resulting property.
    • For an auto-accessor field decorator, this will be a{ get, set } object corresponding to the generatedget method andset method signatures.
    • For a normal field decorator, this will always beundefined.
  • 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.
    • For a class, method, getter, or setter decorator, this will beT.
    • For an auto-accessor field decorator, this will be a{ get?, set?, init? } whoseget andset correspond to the generatedget method andset method signatures. The optionalinit member can be used to
      inject aninitializer mutator function.
    • For a normal field decorator, this can be aninitializer mutator function.

Method Decorators

classMyClass{m():void{ ...}}

Amethod decorator applied tom(): void would use the types

typeT=(this:MyClass)=>void;typeC=&ClassMethodDecoratorContext<MyClass,(this:MyClass)=>void>&{name:"m",private:false,static:false};typeR=(this:MyClass)=>void;

resulting in a call signature like

typeExpectedSignature=(target:(this:MyClass)=>void,context:&ClassMethodDecoratorContext<MyClass,(this:MyClass)=>void>&{name:"m",private:false,static:false},)=>((this:MyClass)=>void)|void;

Here, we specify atarget type (T) of(this: MyClass) => void. We don't normally traffic around thethis type 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:

functionlog<T,Aextendsany[],R>(target:(this:T, ...args:A)=>R,context:ClassMethodDecoratorContext<T,(this:T, ...args:A)=>R>){returnfunction(this:T, ...args:A):R{        console.log(`${context.name.toString()}: enter`);try{// need the appropriate `this`returntarget.call(this, ...args);}finally{console.log(`${context.name.toString()}: exit`);}};}

You may also notice that we intersect a common context type, in this caseClassMethodDecoratorContext, 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.iterator method

functioniteratorWrap<T,V>(target:(this:T)=>Iterable<V>,context:ClassMethodDecoratorContext<T,(this:T)=>Iterable<V>>&{name:Symbol.iterator}){    ...}

, or one that is restricted tostatic fields

functionlazyStatic<T,V>(target:undefined,context:ClassFieldDecoratorContext<T,V>&{static:true}){    ...}

, or one that prohibits usage on private members

functionpublicOnly(target:unknown,context:ClassMemberDecoratorContext&{private:false}){    ...}

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 theaccess property, which acts on thefinal type of the decorated element. The type literal allows us to be flexible with future changes.

Getter and Setter Decorators

classMyClass{getx():string{ ...}setx(value:string){ ...}}

Agetter decorator applied toget x(): string above would have the types

typeT=(this:MyClass)=>string;typeC=ClassGetterDecoratorContext<MyClass,string>&{name:"x",private:false,static:false};typeR=(this:MyClass)=>string;

resulting in a call signature like

typeExpectedSignature=(target:(this:MyClass)=>string,context:ClassGetterDecoratorContext<MyClass,string>&{name:"x",private:false,static:false},)=>((this:MyClass)=>string)|void;

, while asetter decorator applied toset x(value: string) would have the types

typeT=(this:MyClass,value:string)=>void;typeC=ClassSetterDecoratorContext<MyClass,string>{name:"x",private:false,static:false};typeR=(this:MyClass,value:string)=>void;

resulting in a call signature like

typeExpectedSignature=(target:(this:MyClass,value:string)=>void,context:ClassSetterDecoratorContext<MyClass,string>&{name:"x",private:false,static:false},)=>((this:MyClass,value:string)=>void)|void;

Getter andsetter decorators in the Stage 3 decorators proposal differ significantly from TypeScript's legacy decorators. Legacy decorators operated on aPropertyDescriptor, giving you access to both theget andset functions as properties of the descriptor. Stage 3 decorators, however, operate directly on theget andset methods themselves.

Field Decorators

classMyClass{    #x:string= ...;}

Afield decorator applied to a field like#x: string above (i.e., one that does not have a leadingaccessor keyword) would have the types

typeT=undefined;typeC=ClassFieldDecoratorContext<MyClass,string>&{name:"#x",private:true,static:false};typeR=(this:MyClass,value:string)=>string;

resulting in a call signature like

typeExpectedSignature=(target:undefined,context:ClassFieldDecoratorContext<MyClass,string>&{name:"#x",private:true,static:false},)=>((this:MyClass,value:string)=>string)|void;

Thetarget of afield decorator is alwaysundefined, as there is nothing installed on the class or prototype during declaration evaluation. Non-static fields are installed only when an instance is created, whilestatic fields 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:

functionaddOne<T>(target:undefined,context:ClassFieldDecoratorContext<T,number>){returnfunction(this:T,value:number){returnvalue+1;};}classC{    @addOne    @addOnex=1;}newC().x;// 3

This essentially behaves as if the following happened instead:

letf1,f2;classC{static{f1=addOne(undefined,{ ...});f2=addOne(undefined,{ ...})}x=f1.call(this,f2.call(this,1));}

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 ofget/set methods 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.

classMyClass{    accessory:number;}

Anauto-accessor decorator applied to a field likeaccessor y: string above would have the types

typeT=ClassAccessorDecoratorTarget<MyClass,string>;typeC=ClassAccessorDecoratorContext<MyClass,string>&{name:"y",private:false,static:false};typeR=ClassAccessorDecoratorResult<MyClass,string>;

resulting in a call signature like

typeExpectedSignature=(target:undefined,context:ClassFieldDecoratorContext<MyClass,string>&{name:"#x",private:true,static:false},)=>((this:MyClass,value:string)=>string)|void;

Note thatT in the example above is essentially the same as

typeT={get:(this:MyClass)=>string,set:(this:MyClass,value:string)=>void};

, whileR is essentially the same as

typeR={get?:(this:MyClass)=>string,set?:(this:MyClass,value:string)=>void,init?:(this:MyClass,value:string)=>string};

The return value (R) is designed to permit replacement of theget andset methods, as well as injecting aninitializer mutator function like you can with a field.

Class Decorators

classMyClass{m():void{ ...}getx():string{ ...}setx(value:string){ ...}    #x:string;    accessory:number;}

Aclass decorator applied toclass MyClass would use the types

typeT=typeofMyClass;typeC=ClassDecoratorContext<typeofMyClass>&{name:"MyClass"};typeR=typeofMyClass;

resulting in a call signature like

typeExpectedSignature=(target:typeofMyClass,context:ClassDecoratorContext<typeofMyClass>&{name:"MyClass"})=>typeofMyClass|void;

Fixes#48885

chieffancypants, abdonrd, justinfagnani, dnalborczyk, trusktr, codeiotic, bondiano, Nerixyz, vdumbrav, btoo, and 4 more reacted with hooray emojiVap0r1ze reacted with heart emojiwhzx5byb, tonivj5, CarelessInternet, octet-stream, phiresky, shrujalshah28, HigherOrderLogic, ruojianll, lbguilherme, ProTip, and 24 more reacted with rocket emojitonivj5, CarelessInternet, keatkeat87, L2jLiga, dnalborczyk, codeiotic, and vdumbrav reacted with eyes emoji
@fatcerberus
Copy link

fatcerberus commentedSep 18, 2022
edited
Loading

The--experimentalDecorators flag will continue to opt-in to the legacy decorator support

At some point this flag should probably be aliased/renamed tolegacyDecorators or something, since “experimental” tends to imply “bleeding edge” and I can imagine future people unfamiliar with TS’s history blindly enabling it thinking they’re opting into something new and shiny as opposed to what it actually is, old and crusty. 😉

the thought process I’m imagining is essentially, “ooh, I like ES decorators, I wonder if this will give me even cooler decorator features…”

whzx5byb, markhaslam, dnalborczyk, trusktr, ruojianll, shivamd20, and LvChengbin reacted with thumbs up emoji

@rbuckton
Copy link
ContributorAuthor

At some point this flag should probably be aliased/renamed tolegacyDecorators or something, since “experimental” tends to imply “bleeding edge” and I can imagine future people unfamiliar with TS’s history blindly enabling it thinking they’re opting into something new and shiny as opposed to what it actually is, old and crusty. 😉

Maybe aliased, but probably not renamed so as not to break existing consumers. Also, parameter decorators are still experimental.

@fatcerberus
Copy link

fatcerberus commentedSep 19, 2022
edited
Loading

Also, parameter decorators are still experimental.

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”

@ruojianll
Copy link

ruojianll commentedSep 25, 2022
edited
Loading

I have 2 questions:

  1. Why Non-static members currently have no way to access the constructor or prototype during class definition?
  2. Class decorator return a non-constructor value (like{ }) is useful. Could you implement it?

@rbuckton
Copy link
ContributorAuthor

  1. Why Non-static members currently have no way to access the constructor or prototype during class definition?

That is the current behavior of the proposal, but an alternative is being discussed intc39/proposal-decorators#465.

  1. Class decorator return a non-constructor value (like{ }) is useful. Could you implement it?

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.

@rbucktonrbuckton merged commit5b18979 intomainJan 19, 2023
@rbucktonrbuckton deleted the decorators-stage-3 branchJanuary 19, 2023 22:54
@robpalme
Copy link

The ES2022 downlevel wraps the class in a function in order to provide encapsulated access to a couple of private static bindings (_staticExtraInitializers, _static_method_decorators).

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
Copy link
ContributorAuthor

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).

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
Copy link

I just tried this with5.0.0-dev.20230123 and I'm seeing multiple decorator errors

error TS8038: Decorators must come after 'export' or 'export default' in JavaScript files.

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?

JavaScript files

Does this mean typescript files support this syntax?

@jakebailey
Copy link
Member

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.

pflannery and lin72h reacted with thumbs up emoji

@rbuckton
Copy link
ContributorAuthor

In a.ts file, you can have decorators either before or afterexport, since we previously parsed them before theexport keyword in--legacyDecorators, and the Stage 3 proposal places them after theexport (anddefault) keyword. In.js files, decoratorsmust come after theexport/default keyword to comply with the specification.

We still think that placing decoratorsafterexport is a poor design and have spoken with the proposal champion who intends to discuss this at the March TC39 meeting. We will likely ship with the JS support as it is specified for 5.0, and will amend it should the placement change in March. Yes, it would be a breaking change in JS files, but fixing it would be local to your project and wouldn't impact the packages you depend on or packages that depend on you. Also, while native implementations are currently underway, there are currently no shipping implementations of native decorators in any engine yet. This means we have a slightly longer window to discuss decorator placement.

uasan, pflannery, littledan, and seansfkelley reacted with thumbs up emoji

@robpalme
Copy link

I won't argue for using class privates in the implementation. There's just one bit of clarification.

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.

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. thestatic_method_decorators array) get captured by the class and have the same lifetime, even though we can see the inner static initializer does not have further use for them. Most DevTools now show you closure-captured bindings. Here's the retainers Chrome DevTools shows for the current Decorators implementation on the earlier code sample.

image

So if we want to release that memory earlier, we need to release it manually by setting it toundefined in either implementation. It's probably a small amount of memory, so whether it's worth the code to release it is your call.

@asnaeb
Copy link

asnaeb commentedJan 25, 2023
edited
Loading

I noticed that with the nightly builds,ClassFieldDecoratorContext does not provide typechecking anymore when only declaringvoid as decorator's return type

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}

Nightly Playground Link

The same code with a previous version of this PR:
v5.0.0-insiders.20230111 Playground Link

I did not test if this occurs with otherDecoratorContext types and I don't know whether this is the desired behavior or not.

@DanielRosenwasser
Copy link
Member

Oh no - the removal ofaccess means the second type param is unwitnessed when comparing types...

@asnaeb
Copy link

Oh no - the removal ofaccess means the second type param is unwitnessed when comparing types...

I have updated the comment. Declaring a full return type seems to fix this issue

@rbuckton
Copy link
ContributorAuthor

Oh no - the removal ofaccess means the second type param is unwitnessed when comparing types...

I'd hate to remove the type param. The only other option I can think of would be to reintroduceaccess withget/set methods that aren't callable (i.e., unioning both possible definitions). As it is I'd just wait until after the January TC39 meeting and fix it then.

@asnaeb
Copy link

asnaeb commentedJan 25, 2023
edited
Loading

Using the return type instead ofClassFieldDecoratorContext for type checking I am encountering a strange behavior. Consider the following code

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 onClassFieldDecoratorContext as you can see here:v5.0.0-insiders.20230111 Playground Link

@DanielRosenwasser
Copy link
Member

Oh no - the removal ofaccess means the second type param is unwitnessed when comparing types...

I'd hate to remove the type param.

We could declare it asin out to enforce invariance even though it isn't witnessed yet.

interfaceClassFieldDecoratorContext<This=unknown,inoutValue=unknown>

Though I think originally,Value was effectively bivariant.

@asnaeb
Copy link

asnaeb commentedJan 31, 2023
edited
Loading

Oh no - the removal ofaccess means the second type param is unwitnessed when comparing types...

I'd hate to remove the type param. The only other option I can think of would be to reintroduceaccess withget/set methods that aren't callable (i.e., unioning both possible definitions). As it is I'd just wait until after the January TC39 meeting and fix it then.

should we keep watching here for updates on this?

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commentedFeb 1, 2023
edited
Loading

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-minuteaccess member changes.

Any other questions/comments should be filed as new issues. Thanks all!

@microsoftmicrosoft locked asresolvedand limited conversation to collaboratorsFeb 1, 2023
@typescript-bottypescript-bot added Fix AvailableA PR has been opened for this issue labelsAug 5, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account?Sign in.
Reviewers

@JLHwungJLHwungJLHwung left review comments

@sandersnsandersnsandersn approved these changes

@DanielRosenwasserDanielRosenwasserDanielRosenwasser approved these changes

@ahejlsbergahejlsbergahejlsberg approved these changes

@uasanuasanuasan approved these changes

@weswighamweswighamAwaiting requested review from weswigham

@sheetalkamatsheetalkamatAwaiting requested review from sheetalkamat

Labels
Author: TeamFix AvailableA PR has been opened for this issueFor Milestone BugPRs that fix a bug with a specific milestone
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement the updated JS decorators proposal
19 participants
@rbuckton@fatcerberus@ruojianll@AaronFriel@DanielRosenwasser@typescript-bot@jakebailey@JLHwung@linbudu599@asnaeb@jkrems@trusktr@ahejlsberg@KilianKilmister@robpalme@pflannery@sandersn@Jack-Works@uasan

[8]ページ先頭

©2009-2025 Movatter.jp