TypeScript Private Elements vs Private Modifiers
Published: Jul 16, 2025
Last updated: Jul 16, 2025
Private access modifiers in TypeScript
If you've been working with TypeScript for a while, you may be familiar with the public
, private
and protected
access modifiers for class properties and methods:
class Modifiers { public a: string; private b: string; protected c: string; // ... omitted }
These access modifiers enforce the member visibility you may be familiar with if you've worked in other object-oriented languages before.
The public
modifier makes members accessible from anywhere, private
restricts access to only within the same class, and protected
allows access from the class and its subclasses. By default, all class members are public
in TypeScript, so you typically only need to explicitly declare private
and protected
members.
The problem with this approach is that these modifiers only apply in TypeScript. Once the code is transpiled, you can still access the elements on the class as if they were always public.
For an example of this, take the following code:
class Target { private name: string; constructor(name: string) { this.name = name; } call() { this.log(); } private log() { console.log(this.name); } } class A { private target: Target; constructor(target: Target) { this.target = target; } } const target = new Target("name"); export const a = new A(target);
In this code, we've set target
as a private property on our contrived class A
.
Once we transpile the code, we can see the following out:
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.b = exports.a = void 0; class Target { name; constructor(name) { this.name = name; } call() { this.log(); } log() { console.log(this.name); } } class A { target; constructor(target) { this.target = target; } } const target = new Target("name"); exports.a = new A(target);
As we can see, the transpiled code strips the modifiers that were enforced only for type checking purposes.
We can confirm this by making use of this code in the Node REPL:
$ node Welcome to Node.js v24.3.0. Type ".help" for more information. > const {a} = require('./index.js') undefined > a.target.name 'name' >
As you can see, we were able to access both our "private" properties target
, as well as that variable's private property name
.
So what can we do about this?
Taking some wisdom from Effective TypeScript
Let's look at some excerpts from fan-favorite book Effective TypeScript:
"TypeScript's visibility modifiers only discourage you from accessing private data. You can even access a private property from within TypeScript using a type assertion or iteration...
ES2022 officially added support for private fields. Unlike TypeScript's private, ECMAScript's private is enforced both for type checking and at runtime. To use it, prefix your class property with a #...
What about public and protected? In JavaScript (and TypeScript), public is the default visibility so there's no need to annotate this explicitly. And while private implies encapsulation, protected implies inheritance. The general rule in object-oriented programming is to prefer composition over inheritance, so practical uses of protected are quite rare. readonly as a field modifier is a type-level construct and is fine to use... A field may be both #private and readonly.
In summary, the book recommends that we stick to the #private
convention and highlights an important point about the protected
modifier: object-oriented programming should prefer composition over inheritance.
Private elements in JavaScript
As we saw in the previous section, ECMAScript introduced the private elements with ES2022.
Let's rewrite our previous example to make use of this:
class Target { #name: string; constructor(name: string) { this.#name = name; } call() { this.log(); } private log() { console.log(this.#name); } } class A { #target: Target; constructor(target: Target) { this.#target = target; } call() { this.#target.call(); } } const target = new Target("name"); export const a = new A(target);
Our contrived classes now replace the private
access modifier with the pound #
prefix for our private elements.
Our transpiled code looks like the following:
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.a = void 0; class Target { #name; constructor(name) { this.#name = name; } call() { this.log(); } log() { console.log(this.#name); } } class A { #target; constructor(target) { this.#target = target; } call() { this.#target.call(); } } const target = new Target("name"); exports.a = new A(target);
Let's now see what happens in the Node REPL:
$ node Welcome to Node.js v24.3.0. Type ".help" for more information. > const {a} = require('./index.js') undefined > a.#target.#name a.#target.#name ^ Uncaught SyntaxError: Private field '#target' must be declared in an enclosing class > a.call() name undefined
As we can see, we get true access control with the pound #
prefix and must access our private elements through public methods.
Conclusion
In general, it would be wise to opt for ECMAScript standards and make use of the pound #
prefix introduced in ES2022 (regardless of how it feels compared to using the keyword).
The private elements approach provides true encapsulation at runtime, making your code more secure and predictable. As highlighted with protected
, chances are that if you are using it, you are likely not abiding by the principle of preferring composition over inheritance.
Consider using private elements (#
) when you need true privacy that persists after transpilation, and reserve TypeScript's private
modifier for cases where you only need compile-time type checking.
Links and Further Reading
Photo credit: timmarshall
TypeScript Private Elements vs Private Modifiers
Introduction