TypeScript: ECMAScript Private Fields for Hard Privacy in Classes
This month on February 20th, Microsoft announced the final release of TypeScript 3.8. It has a bunch of new features. One interesting feature is the support for the ECMAScript private fields that are described in this proposal.
With private fields, you get encapsulation that is natively supported in JavaScript. Look at the Friend
class below. It has a #firstName
private field. You create a private field by starting the name with a hash (#), that’s it. In the constructor of the Friend
class the private field #firstName
is set to the firstName
constructor parameter. Then there’s a getFullName
method that returns the value of that #firstName
private field.
class Friend {
#firstName: string;
constructor(firstName: string) {
this.#firstName = firstName;
}
getFullName() {
return this.#firstName;
}
}
When you use the Friend
class, it works as expected. You can access the getFullName
method, but not the #firstName
field, as it is a private field.
let friend = new Friend('Thomas');
console.log(friend.getFullName()); // Works
console.log(friend.#firstName); // Does not work, as it is a private field
The maximum target of the TypeScript 3.8 compiler is ES2020. But private fields are a stage 3 proposal, which means they didn’t make it into the ES2020 standard. That means with TypeScript 3.8 you need to set the compiler target to ESNEXT to see private fields in the generated JavaScript code.
When you set the target of the compiler to ESNEXT, the TypeScript compiler generates this JavaScript code for the Friend
class:
class Friend {
constructor(firstName) {
this.#firstName = firstName;
}
#firstName;
getFullName() {
return this.#firstName;
}
}
Yes, that’s above is JavaScript, and as you can see, private fields are natively supported in JavaScript. That means even if you use the Friend
class in pure JavaScript, you’ll get an error if you try to access the #firstName
field from outside of the Friend
class like below:
console.log(friend.#firstName); // Does also not work in pure JavaScript
TypeScript supports downleveling of private fields. If you use private fields, you need to specify as a minimum target ES2015. With something between ES2015 and ES2020 specified as a target, the TypeScript compiler generates this JavaScript output for the Friend
class:
"use strict";
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 _firstName;
class Friend {
constructor(firstName) {
_firstName.set(this, void 0);
__classPrivateFieldSet(this, _firstName, firstName);
}
getFullName() {
return __classPrivateFieldGet(this, _firstName);
}
}
_firstName = new WeakMap();
As you can see in the last line, a WeakMap
is used for the private firstName
field.
But wait… I can create a property with a private access modifier. Why private fields?
Great question. The access modifiers private
, protected
and public
are a TypeScript thing, they’re not part of JavaScript. In TypeScript, you can build the Friend
class also with a private firstName
property like this:
class Friend {
private firstName: string;
constructor(firstName: string) {
this.firstName = firstName;
}
getFullName() {
return this.firstName;
}
}
As long as you stay in TypeScript, you’re fine, as you’re getting errors when you access the private firstName
property from outside of the Friend
class:
let friend = new Friend('Thomas');
console.log(friend.firstName); // TypeScript gives you an error here,
// as it is a private property
But now let’s look at the JavaScript output. Let’s compile with ES2015 or higher, as ES2015 has support for classes in JavaScript. With ES2015 or higher, you get this class in JavaScript:
class Friend {
constructor(firstName) {
this.firstName = firstName;
}
getFullName() {
return this.firstName;
}
}
Now the important point: If you use the access modifiers public
or protected
for the firstName
property in TypeScript, the JavaScript class above will look exactly the same. Or, let’s say it in other words: In JavaScript, the firstName
property is always public. That means the following code is fine in JavaScript:
let friend = new Friend('Thomas');
console.log(friend.firstName); // Works in JavaScript like a charm
That’s the big difference between an ECMAScript private field and a property that is using TypeScript’s private
access modifier: ECMAScript private fields are a native JavaScript feature, which means even pure JavaScript will give you an error if you try to access a private field from outside of the class. Not being able to access that field from JavaScript is also known as hard privacy.
Now, when you define such a private field in TypeScript, you can’t set an access modifier like public
or private
on it, as it is always private.
Microsoft mentioned in the TypeScript 3.8 announcement some of the most important rules of private fields:
Private fields start with a
#
character. Sometimes we call these private names.Every private field name is uniquely scoped to its containing class.
TypeScript accessibility modifiers like
public
orprivate
can’t be used on private fields.Private fields can’t be accessed or even detected outside of the containing class – even by JavaScript users! Sometimes we call this hard privacy.
https://devblogs.microsoft.com/typescript/announcing-typescript-3-8/#ecmascript-private-fields
What should you use: private fields or properties with private access modifier?
It depends! Private fields mean that no user of your code can access the field. If you create a normal property with private
access modifier, the property is accessible in JavaScript from outside of the class, which could be a problem, but it could also be useful in some situations.
If you want real encapsulation and hard privacy, you might want to go with private fields. But before you do that, there’s more to consider.
For private fields, you need to target ES2015 or higher
If you use private fields in TypeScript, you need to target ES2015 or higher. But with a property with a private
access modifier you can target even the lowest version, which is ES3.
Private fields are a bit slower
If performance is important, be aware that private fields are downleveled with WeakMaps. And right now, WeakMaps are not as fast as pure properties.
Wrap up
Overall, I think private fields are a great feature of ECMAScript, and in TypeScript, you can use them already today. But consider the thoughts above.
Happy coding,
Thomas
Leave a Reply