Skip to content

ES private class elements - #42458

Merged
rbuckton merged 87 commits into
microsoft:masterfrom
bloomberg:es-private-methods-and-accessors
Mar 25, 2021
Merged

ES private class elements#42458
rbuckton merged 87 commits into
microsoft:masterfrom
bloomberg:es-private-methods-and-accessors

Conversation

@dragomirtitian

@dragomirtitiandragomirtitian commented Jan 22, 2021

Copy link
Copy Markdown
Contributor

Private Class Elements

This PR implements the TC39 Stage-3 Private Methods and Accessors proposal as well as TC39 Stage 3 Static Class features.

Fixes#39066, #37677, #42985

Remaining Work

  • Resolve open questions (see below)
  • PR the finalized helpers to tslib
  • Validate limitations below

Downlevel

WeakSets are used for instance private methods and accessors, in the same way that the existing Private Fields downlevel uses WeakMaps.

Instance Private Methods and Accessors

TypeScript

classSquare{
#size: number;constructor(size: number){this.#size =size;}get #diagonal(){returnMath.hypot(this.#size,this.#size);}area(){returnMath.pow(this.#diagonal,2)/2;}}constsquare=newSquare(7);console.log(square.area());// logs 49

JavaScript - ES2020 emit

var__classPrivateFieldSet=(this&&this.__classPrivateFieldSet)||function(receiver,privateMap,value){if(!privateMap.has(receiver)){thrownewTypeError("attempted to set private field on non-instance");}privateMap.set(receiver,value);returnvalue;};var__classPrivateFieldGet=(this&&this.__classPrivateFieldGet)||function(receiver,privateMap){if(!privateMap.has(receiver)){thrownewTypeError("attempted to get private field on non-instance");}returnprivateMap.get(receiver);};var__classPrivateAccessorGet=(this&&this.__classPrivateAccessorGet)||function(receiver,instances,fn){if(!instances.has(receiver)){thrownewTypeError("attempted to get private accessor on non-instance");}returnfn.call(receiver);};var_Square_size,_Square_diagonal_get,_Square_instances;classSquare{constructor(size){_Square_instances.add(this);_Square_size.set(this,void0);__classPrivateFieldSet(this,_Square_size,size);}area(){returnMath.pow(__classPrivateAccessorGet(this,_Square_instances,_Square_diagonal_get),2)/2;}}_Square_size=newWeakMap(),_Square_instances=newWeakSet(),_Square_diagonal_get=function_Square_diagonal_get(){returnMath.hypot(__classPrivateFieldGet(this,_Square_size),__classPrivateFieldGet(this,_Square_size));};constsquare=newSquare(7);console.log(square.area());// logs 49
Static Private Fields, Methods and Accessors
classA{static #field =10static #method() : void{}// no errorstaticget #roProp(){return0;}staticset #woProp(value: number){}staticget #prop(){return0}staticset #prop(value: number){}statictest(){console.log(A.#field);A.#field =10;A.#method();console.log(A.#roProp);A.#roProp =10console.log(A.#woProp);A.#woProp =10console.log(A.#prop);A.#prop =10}}

JavaScript - ES2020 emit

var__classStaticPrivateFieldGet=(this&&this.__classStaticPrivateFieldGet)||function(receiver,classConstructor,propertyDescriptor){if(receiver!==classConstructor){thrownewTypeError("Private static access of wrong provenance");}if(propertyDescriptor===undefined){thrownewTypeError("Private static field was accessed before its declaration.");}returnpropertyDescriptor.value;};var__classStaticPrivateFieldSet=(this&&this.__classStaticPrivateFieldSet)||function(receiver,classConstructor,propertyDescriptor,value){if(receiver!==classConstructor){thrownewTypeError("Private static access of wrong provenance");}if(propertyDescriptor===undefined){thrownewTypeError("Private static field was accessed before its declaration.");}propertyDescriptor.value=value;returnvalue;};var__classStaticPrivateMethodGet=(this&&this.__classStaticPrivateMethodGet)||function(receiver,classConstructor,fn){if(receiver!==classConstructor){thrownewTypeError("Private static access of wrong provenance");}returnfn;};var__classStaticPrivateAccessorGet=(this&&this.__classStaticPrivateAccessorGet)||function(receiver,classConstructor,fn){if(receiver!==classConstructor){thrownewTypeError("Private static access of wrong provenance");}returnfn.call(receiver);};var__classStaticPrivateReadonly=(this&&this.__classStaticPrivateReadonly)||function(){thrownewTypeError("Private static element is not writable");};var__classStaticPrivateWriteonly=(this&&this.__classStaticPrivateWriteonly)||function(){thrownewTypeError("Private static element is not readable");};var__classStaticPrivateAccessorSet=(this&&this.__classStaticPrivateAccessorSet)||function(receiver,classConstructor,fn,value){if(receiver!==classConstructor){thrownewTypeError("Private static access of wrong provenance");}fn.call(receiver,value);returnvalue;};var_A_field,_A_method,_A_roProp_get,_A_woProp_set,_A_prop_get,_A_prop_set;classA{statictest(){console.log(__classStaticPrivateFieldGet(A,A,_A_field));__classStaticPrivateFieldSet(A,A,_A_field,10);__classStaticPrivateMethodGet(A,A,_A_method).call(A);console.log(__classStaticPrivateAccessorGet(A,A,_A_roProp_get));__classStaticPrivateReadonly(A,10);console.log(__classStaticPrivateWriteonly(A));__classStaticPrivateAccessorSet(A,A,_A_woProp_set,10);console.log(__classStaticPrivateAccessorGet(A,A,_A_prop_get));__classStaticPrivateAccessorSet(A,A,_A_prop_set,10);}}_A_method=function_A_method(){},_A_roProp_get=function_A_roProp_get(){return0;},_A_woProp_set=function_A_woProp_set(value){},_A_prop_get=function_A_prop_get(){return0;},_A_prop_set=function_A_prop_set(value){};_A_field={value: 10};

References

Wider Contributions

Development of this PR led to these bug discoveries and fixes in other projects:

Credits

This PR includes contributions from the following Bloomberg engineers:

Design Limitations & Open Questions

  1. The pre-existing class fields transform can produce valid JavaScript from a syntactically invalid input. One example of this is duplicate private names. A similar issue will be visible in #constructor test since we now transform private methods. What is the desired TypeScript behavior in this case?

  2. This implementation does not work with the experimental Decorators in TypeScript. There is no spec for the interaction between these two features and implementing something non-standard that is likely to break in the future does not seem useful. Therefore we issue an error if a Decorators is used on a class containing a static private field/method/accessor.

Example
functiondec(){returnfunction(cls: new(...a: any)=>any){returnclassextendscls{staticsomeField=11;constructor(...a: any){super(...a);}}}}
@dec()classFoo{staticsomeField=10;static #somePrivateField =10;staticm(){// displays 11 because decorator redefined it// and all references to Foo were rewritten to point to // whatever the decorator returnedFoo.someField// Right now this would always fail, since Foo will be rewritten// to point to whatever dec returned, so it will not point to the // class that actually defined `#somePrivateField`Foo.#somePrivateField }}
  1. Initializers are not allowed if target: esnext AND useDefineForClassFields: false. This is due to the fact that initializing private fields outside of a class is a non-trivial transform (a possible solution is described here - a modified version of it could be applied if there is desire for this combination to allow it) and keeping the init in the class would change runtime ordering of initializers.

  2. this is not allow in static initialization expressions. The restriction is a pre-existing issue and so is considered out-of-scope of this PR.

  3. Unlike instance #private class elements, static #private class elements do not depend on WeakMap or WeakSet. This means that technically we could transpile static #private class elements for es5 or even es3. However having different requirements for instance vs static #private class elements would probably cause more confusion than benefit. Therefore we retain the existing minimum target of es2015 for using static #private class elements and will error otherwise.

@typescript-bottypescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Jan 22, 2021
@mkubilayk
mkubilaykforce-pushed the es-private-methods-and-accessors branch from eff2eda to fb73ccdCompareFebruary 3, 2021 11:00
dragomirtitianand others added 28 commits February 3, 2021 18:44
…ing except classes.
Changes objects literal checking to not bail on first private name found in object literal.
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Private methods inside class expressions should not error.
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
…y assignment
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
@mkubilayk

Copy link
Copy Markdown
Contributor

Created microsoft/tslib#146 for tslib updates.

@rbuckton thanks for the review again. I think this is ready for another round.

Comment threadsrc/compiler/transformers/classFields.ts Outdated

@rbucktonrbuckton left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposed changes (as well as relevant changes in createPrivateIdentifierAssignment) should address the comment issue.

Comment threadsrc/compiler/transformers/classFields.ts Outdated
Comment threadsrc/compiler/transformers/classFields.ts Outdated
@mkubilayk

Copy link
Copy Markdown
Contributor

@rbuckton Thanks a lot for taking the time to debug this. I made the changes you suggested.


// leave invalid code untransformed
const info = accessPrivateIdentifier(node.name);
Debug.assert(info, "Undeclared private name for property declaration.");

@rbucktonrbucktonMar 25, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can a user write source text that would fail this assertion?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. That would indicate a bug in addPrivateIdentifierToEnvironment.

@rbuckton
rbuckton merged commit e638af7 into microsoft:masterMar 25, 2021
@rbuckton

Copy link
Copy Markdown
Contributor

This has been a tremendous effort. I'm happy to see these changes go in and look forward to the community having a chance to test them in nightlies starting tomorrow!

@Kingwl

Copy link
Copy Markdown
Contributor

Congrats! Great work!

@orta

orta commented Mar 25, 2021

Copy link
Copy Markdown
Contributor

The nightly playground build just shipped for folks interested in playing with it. Also congrats, serious work this.

@danieltroger

Copy link
Copy Markdown

Hello, why is vs code still saying "A method cannot be named with a private identifier."? Shouldn't it stop since this PR has been merged since quite some time now?

@acutmore

Copy link
Copy Markdown
Contributor

Hello, why is vs code still saying "A method cannot be named with a private identifier."? Shouldn't it stop since this PR has been merged since quite some time now?

Hi @danieltroger, private methods will be part of TypeScript 4.3 which is still in Beta.

@danieltroger

Copy link
Copy Markdown

@acutmore I see, thank you for the swift reply. Any idea when it will be released? I feel like it will, even after the release, still take a long time until it lands in editors :(

Is it easy to switch to beta? I'm mainly using atom and https://github.com/TypeStrong/atom-typescript

@acutmore

Copy link
Copy Markdown
Contributor

@acutmore I see, thank you for the swift reply. Any idea when it will be released? I feel like it will, even after the release, still take a long time until it lands in editors :(

4.3 is likely to be released at the end of May #42762
https://devblogs.microsoft.com/typescript/announcing-typescript-4-3-beta/#whats-next

Is it easy to switch to beta? I'm mainly using atom and https://github.com/TypeStrong/atom-typescript

The atom-typescript docs say you can use a specific version of TypeScript by installing it.
https://github.com/TypeStrong/atom-typescript/blob/master/docs/faq.md#which-version-of-typescript-does-atom-typescript-use

npm install typescript@4.3.0-beta

@danieltroger

danieltroger commented May 4, 2021

Copy link
Copy Markdown

Yo it worked, finally no red, thanks a lot! I got into npm conflict hell tho

@mahnunchik

Copy link
Copy Markdown

When is ETA? It is horrible microsoft/vscode#106351

@acutmoreacutmore mentioned this pull request Jun 18, 2021
1 task
@dragomirtitian
dragomirtitian deleted the es-private-methods-and-accessors branch November 17, 2023 15:56
@microsoftmicrosoft locked as resolved and limited conversation to collaborators Oct 16, 2025
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

For Uncommitted BugPR for untriaged, rejected, closed or missing bug

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

Add support for ES.Next private methods and private accessor properties

11 participants

@dragomirtitian@typescript-bot@rbuckton@ExE-Boss@mkubilayk@Kingwl@orta@danieltroger@acutmore@mahnunchik@sandersn