Typescript offers the public
,protected
and private
keywords for defining the visibility of the member or the method declared next to them, however, I know that since ES6 Javascript allow the use of the prefix “#” to a class member or method in order to achieve the same result.
To try to have a better understanding of how all works behind the scenes I wrote a toy-class in Typescript just to see how it compiles in javascript:
class aClass { #jsPrivate: number; get jsPrivate() { return this.#jsPrivate}; private tsPrivate: number; protected tsProtected: number; public tsPublic: number; constructor( a: number, b: number, c: number, d: number) { this.#jsPrivate = a; this.tsPrivate = b; this.tsProtected = c; this.tsPublic = d; } } console.log(new aClass(1,2,3,4));
that compiled using tsc --target es6
with Typescript version 4.3.5 becomes:
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _aClass_jsPrivate; class aClass { constructor(a, b, c, d) { _aClass_jsPrivate.set(this, void 0); __classPrivateFieldSet(this, _aClass_jsPrivate, a, "f"); this.tsPrivate = b; this.tsProtected = c; this.tsPublic = d; } get jsPrivate() { return __classPrivateFieldGet(this, _aClass_jsPrivate, "f"); } ; } _aClass_jsPrivate = new WeakMap(); console.log(new aClass(1, 2, 3, 4));
I’m not sure if I did all correct, but I notice that the js style private member is now in the global scope, and also the members declared using typescript modifiers are now all public, although theoretically any attempt to access a private member should be caught while compiling to javascript, I’m not sure that’s the best for code security.
Do you have any advice on which one is the best way to modify member visibility?
Could you also explain to me why there are those differences?
Advertisement
Answer
JavaScript private field syntax #
Support
This is not official yet. The syntax is included in the latest upcoming draft of the specifications. However, the ES2021 specification (ES12) does not have it. So right now it is in a transitioning state to become official.
In the meantime, not all browsers support private fields. Most notably Firefox version 89 (as of writing this, current version for the browser) does not support it. The upcoming version 90 would add support for private fields but it is in Beta.
Access level
Private field syntax only allows hiding a field. There is no notion in JavaScript for protected access (only visible to descendants of the class). So for any code outside the class, a field is either visible to anybody or not visible. Nothing in between.
In addition, private fields in JavaScript are completely hidden. There is no official mechanism for extracting and interacting with them programmatically from the outside. Only the class that declared it can use them.
class Foo { #id; constructor(num) { this.#id = num; } viewPrivate(other) { return other.#id; } } class Bar { #id; constructor(num) { this.#id = num; } } const foo1 = new Foo(1); const foo2 = new Foo(2); console.log(foo1.viewPrivate(foo1)); //1 console.log(foo1.viewPrivate(foo2)); //2 const bar = new Bar(3); console.log(foo1.viewPrivate(bar)); //Error // cannot access #x from different class
TypeScript access modifiers
Support
The TypeScript access modifiers are technically supported everywhere. That is because the TypeScript code is converted to plain JavaScript. The compiler can be configured which ECMAScript version should be targetted.
As with other parts of the type system, the access modifiers will be stripped at compile time. If you tried to access a field where you should not be able to, you would get a compilation error.
Access level
The biggest difference is the support for protected access level to allow a field to be reachable from subclasses:
class Foo { public a = 1; protected b = 2; private c = 3; } class Bar extends Foo { doStuff(): number { return this.a + //OK - it's public this.b + //OK - it's protected and Bar extends Foo this.c; //Error - it's private } }
Another big difference compared to plain JavaScript is that TypeScript access modifiers can be changed in subclasses to be less restrictive:
class A { protected x = "hello"; } class B extends A { public x = "world"; } console.log(new A().x); // Compilation error console.log(new B().x); // OK
Finally, I have to double down on how different JavaScript’s private fields are to TypeScript’s private
access modifier. TypeScript does not “hide” fields. It prevents you from referencing them. The fields still exist and could be accessed normally by code. Even compilation errors can be prevented:
class Foo { private secret = "top secret"; } const foo = new Foo(); console.log(JSON.stringify(foo)); //"{"secret":"top secret"}" console.log((foo as any).secret); //"top secret"
This cannot happen with the private field syntax in JavaScript. Again, there private fields are completely hidden from the outside.
Which one to use
This is down to choice. If writing OOP focused TypeScript code, you might want to just stick to private
, protected
, public
keywords. They play nicer with the class hierarchies.
With that said, the private field syntax #
in JavaScript can be really powerful if you want stronger encapsulation that will not leak out.
You can also mix the two types of encapsulation.
At the end of the day, it would be case-by-case basis for each.