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