Use cases: #
-private fields
Preface:
Compile-time and run-time privacy
#
-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.
Safe class inheritance
#
-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.
External libraries
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.
JS API omits #
-private fields
Built-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" ]
Use cases: private
keyword
Preface:
Access to internal class API and state (compile-time only privacy)
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.
Available in all ES environments
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
.
Consistency and compatibility
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.
Other reasons
private
might provide better run-time performance in some down-leveling cases (see here).
- There are no hard private class methods available in TS up to now.
- Some people like the
private
keyword notation better 😊.
Note on both
Both approaches create some kind of nominal or branded type at compile-time.
class A1 { private a = 0; }
class A2 { private a = 42; }
const a: A1 = new A2();
// error: "separate declarations of a private property 'a'"
// same with hard private fields
Also, both allow cross-instance access: an instance of class A
can access private members of other A
instances:
class A {
private a = 0;
method(arg: A) {
console.log(arg.a); // works
}
}
Sources