In TypeScript 3.8+, what are the differences between using the private keyword to mark a member private:
class PrivateKeywordClass {
private
#-private fieldsPreface:
#-private, hard private, run-time private#-private fields provide compile-time and run-time privacy, which is not "hackable". It is a mechanism to prevent access to a member from outside the class body in any direct way.
class A {
#a: number;
constructor(a: number) {
this.#a = a;
}
}
let foo: A = new A(42);
foo.#a; // error, not allowed outside class bodies
(foo as any).#bar; // still nope.
#-private fields get a unique scope. Class hierarchies can be implemented without accidental overwrites of private properties with equal names.
class A {
#a = "a";
fnA() { return this.#a; }
}
class B extends A {
#a = "b";
fnB() { return this.#a; }
}
const b = new B();
b.fnA(); // returns "a" ; unique property #a in A is still retained
b.fnB(); // returns "b"
TS compiler fortunately emits an error, when private properties are in danger of being overwriten (see this example). But due to the nature of a compile-time feature everything is still possible at run-time, given compile errors are ignored and/or emitted JS code utilized.
Library authors can refactor #-private identifiers without causing a breaking change for clients. Library users on the other side are protected from accessing internal fields.
#-private fieldsBuilt-in JS functions and methods ignore #-private fields. This can result in a more predictable property selection at run-time. Examples: Object.keys, Object.entries, JSON.stringify, for..in loop and others (code sample; see also Matt Bierner's answer):
class Foo {
#bar = 42;
baz = "huhu";
}
Object.keys(new Foo()); // [ "baz" ]
private keywordPreface:
private members of a class are conventional properties at run-time. We can use this flexibility to access class internal API or state from the outside. In order to satisfy compiler checks, mechanisms like type assertions, dynamic property access or @ts-ignore may be used amongst others.
Example with type assertion (as / <>) and any typed variable assignment:
class A {
constructor(private a: number) { }
}
const a = new A(10);
a.a; // TS compile error
(a as any).a; // works
const casted: any = a; casted.a // works
TS even allows dynamic property access of a private member with an escape-hatch:
class C {
private foo = 10;
}
const res = new C()["foo"]; // 10, res has type number
Where can private access make sense? (1) unit tests, (2) debugging/logging situations or (3) other advanced case scenarios with project-internal classes (open-ended list).
Access to internal variables is a bit contradictory - otherwise you wouldn't have made them private in the first place. To give an example, unit tests are supposed to be black/grey boxes with private fields hidden as implementation detail. In practice though, there may be valid approaches from case to case.
TS private modifiers can be used with all ES targets. #-private fields are only available for target ES2015/ES6 or higher. In ES6+, WeakMap is used internally as downlevel implementation (see here). Native #-private fields currently require target esnext.
Teams might use coding guidelines and linter rules to enforce the usage of private as the only access modifier. This restriction can help with consistency and avoid confusion with the #-private field notation in a backwards-compatible manner.
If required, parameter properties (constructor assignment shorthand) are a show stopper. They can only be used with private keyword and there are no plans yet to implement them for #-private fields.
private might provide better run-time performance in some down-leveling cases (see here).private keyword notation better