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.

Photo credit: timmarshall

Personal image

Dennis O'Keeffe

Byron Bay, Australia

Dennis O'Keeffe

2020-present Dennis O'Keeffe.

All Rights Reserved.