ES private class elements by dragomirtitian · Pull Request #42458 · microsoft/TypeScript (original) (raw)
Private Class Elements
This PR implements the TC39 Stage-3 Private Methods and Accessors proposal as well as TC39 Stage 3 Static Class features.
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
class Square { #size: number; constructor(size: number) { this.#size = size; } get #diagonal() { return Math.hypot(this.#size, this.#size); } area() { return Math.pow(this.#diagonal, 2) / 2; } } const square = new Square(7); console.log(square.area()); // logs 49
JavaScript - ES2020 emit
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to set private field on non-instance"); } privateMap.set(receiver, value); return value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to get private field on non-instance"); } return privateMap.get(receiver); }; var __classPrivateAccessorGet = (this && this.__classPrivateAccessorGet) || function (receiver, instances, fn) { if (!instances.has(receiver)) { throw new TypeError("attempted to get private accessor on non-instance"); } return fn.call(receiver); }; var _Square_size, _Square_diagonal_get, _Square_instances; class Square { constructor(size) { _Square_instances.add(this); _Square_size.set(this, void 0); __classPrivateFieldSet(this, _Square_size, size); } area() { return Math.pow(__classPrivateAccessorGet(this, _Square_instances, _Square_diagonal_get), 2) / 2; } } _Square_size = new WeakMap(), _Square_instances = new WeakSet(), _Square_diagonal_get = function _Square_diagonal_get() { return Math.hypot(__classPrivateFieldGet(this, _Square_size), __classPrivateFieldGet(this, _Square_size)); }; const square = new Square(7); console.log(square.area()); // logs 49
Static Private Fields, Methods and Accessors
class A { static #field = 10 static #method() : void {} // no error
static get #roProp() { return 0; }
static set #woProp(value: number) { }
static get #prop() { return 0 }
static set #prop(value: number) { }
static test() {
console.log(A.#field);
A.#field = 10;
A.#method();
console.log(A.#roProp);
A.#roProp = 10
console.log(A.#woProp);
A.#woProp = 10
console.log(A.#prop);
A.#prop = 10
}
}
JavaScript - ES2020 emit
var __classStaticPrivateFieldGet = (this && this.__classStaticPrivateFieldGet) || function (receiver, classConstructor, propertyDescriptor) { if (receiver !== classConstructor) { throw new TypeError("Private static access of wrong provenance"); } if (propertyDescriptor === undefined) { throw new TypeError("Private static field was accessed before its declaration."); } return propertyDescriptor.value; }; var __classStaticPrivateFieldSet = (this && this.__classStaticPrivateFieldSet) || function (receiver, classConstructor, propertyDescriptor, value) { if (receiver !== classConstructor) { throw new TypeError("Private static access of wrong provenance"); } if (propertyDescriptor === undefined) { throw new TypeError("Private static field was accessed before its declaration."); } propertyDescriptor.value = value; return value; }; var __classStaticPrivateMethodGet = (this && this.__classStaticPrivateMethodGet) || function (receiver, classConstructor, fn) { if (receiver !== classConstructor) { throw new TypeError("Private static access of wrong provenance"); } return fn; }; var __classStaticPrivateAccessorGet = (this && this.__classStaticPrivateAccessorGet) || function (receiver, classConstructor, fn) { if (receiver !== classConstructor) { throw new TypeError("Private static access of wrong provenance"); } return fn.call(receiver); }; var __classStaticPrivateReadonly = (this && this.__classStaticPrivateReadonly) || function () { throw new TypeError("Private static element is not writable"); }; var __classStaticPrivateWriteonly = (this && this.__classStaticPrivateWriteonly) || function () { throw new TypeError("Private static element is not readable"); }; var __classStaticPrivateAccessorSet = (this && this.__classStaticPrivateAccessorSet) || function (receiver, classConstructor, fn, value) { if (receiver !== classConstructor) { throw new TypeError("Private static access of wrong provenance"); } fn.call(receiver, value); return value; };
var _A_field, _A_method, _A_roProp_get, _A_woProp_set, _A_prop_get, _A_prop_set; class A { static test() { 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() { return 0; }, _A_woProp_set = function _A_woProp_set(value) { }, _A_prop_get = function _A_prop_get() { return 0; }, _A_prop_set = function _A_prop_set(value) { }; _A_field = { value: 10 };
References
- Spec
- Private instance fields PR in TypeScript
- Engines
- V8 8.4 blog
- Chrome shipped in v84
- Safari has unflagged upstream support
- FireFox has support in v81 behind a flag
- Babel
Wider Contributions
Development of this PR led to these bug discoveries and fixes in other projects:
- Babel issues reported
- Get operation on private accessor without getter should throw babel/babel#12673
- Right-hand side expression is not evaluated on assignment to private method babel/babel#12705
- Using a set/get accessor causes a runtime error if the descriptor is not yet initialized babel/babel#12905
- Confusing error when static private field is accessed before declaration. babel/babel#12904
- V8 issues reported
Credits
This PR includes contributions from the following Bloomberg engineers:
- Titian Cernicova-Dragomir @dragomirtitian: checker, language service, PR review, JS Emit
- Kubilay Kahveci @mkubilayk: JS emit
- Joey Watts @joeywatts: PR review
- Rob Palmer @robpalme: advice, spec expertise
- Tim McClure @tim-mc: JS emit advice
Design Limitations & Open Questions
- 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? - 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
function dec() { return function (cls: new (...a: any) => any) { return class extends cls { static someField = 11; constructor(...a: any) { super(...a); } } } }
@dec() class Foo { static someField = 10; static #somePrivateField = 10; static m() { // displays 11 because decorator redefined it // and all references to Foo were rewritten to point to // whatever the decorator returned Foo.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
}
}
- Initializers are not allowed if
target: esnext
ANDuseDefineForClassFields: 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. 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.- Unlike instance #private class elements, static #private class elements do not depend on
WeakMap
orWeakSet
. This means that technically we could transpile static #private class elements fores5
or evenes3
. 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 ofes2015
for using static #private class elements and will error otherwise.