TypeScript encapsulation demystified

Picture of a drink.
Photo by veeterzy on Unsplash

TypeScript as a language is always ahead of the curve in favor of JavaScript. It brings ECMAScript features much faster than they become a standard or browsers implement them. This gives us time to prepare ourselves and our projects for the future and these new awesome features.

Some time ago to be more specific exactly 26 months ago, on a 20th Feb 2020 a new version of Typescript (3.8) has been officially released. It introduced the great feature of private field standard. At that time I felt that the hype for it was quite big. Now the dust has settled so we can talk about it and think if it is a good idea to finally start using the "hash" privates.

In this article, I will focus on presenting the benefits and downsides of private field standard. I would like to share my opinion on the matter and answer the question if it's worth using. If you want to read more about the feature there is a great article from Matthew MacDonald TypeScript 3.8 Finally Fixes Private Fields.

Encapsulation paradigm

In my opinion, encapsulation is one of the most important paradigms of the Object Oriented Programming. TypesScript implements all of the OPP paradigms, but I have always felt that it does it in a different way than other similar languages like Java or C#. Encapsulation is the perfect example of a mentioned different approach. At the Typescript level, you can use access modifiers public, protected, private, but after transpilation the JavaScript code doesn't recognize it. Classes in JavaScript have no support for access modifiers, it simply disappears from the source code. Let's have a look at the code:

The basic class before and after transpilation from TypeScript to JavaScript.
The basic class before and after transpilation from TypeScript to JavaScript.

As you can see there is no privacy. The private properties after transpilation to JavaScript simply become a public one.

This is not the value of the private variable you are looking for.
This is not the value of the private variable you are looking for.

This inconvenient behavior brings potential issues. It turns out that you can easily modify private properties, without any errors from the compiler or during runtime.

class Cat {

  private name: string;

}

let cat = new Cat();
(cat as any).name = 'Coconut' // This works
cat['name'] = 'Coconut' // This also works

This lack of class encapsulation is especially worth mentioning when we think of the bigger picture e.g. possible security vulnerabilities.

True private

Since TypeScript 3.8 we can use the new "hard" private access modifier, which fixes the previously mentioned issue of lack of true encapsulation. It is also very easy to use instead of typing access modifier private use simple #, like that:

class Cat {

  #name: string = 'Coconut';

}

Now you can be sure that nobody can access your privately scoped fields. Accessing the private property during JavaScript runtime doesn't work. Neither trying to get value with the bracket notation. TypeScript compiler will not allow to do it, take a look at the message below.

let cat = new Cat();

(cat as any).#name = 'Pineapple' // This doesn't work
cat['#name'] = 'Pineapple' // This also doesn't work
TypeScript compilation Error: Private identifiers are not allowed outside class bodies.
TypeScript compilation Error: Private identifiers are not allowed outside class bodies.

Another good news is that as of today, private fields are supported by all of the major browsers, so you can start using them in your project right away.

Private class fields browser support from caniuse.com
Private class fields browser support from caniuse.com

Transpilation

The true support for private fields comes in the es10 standard (that is ECMAScript 2022). In most of the projects, transpilation target in the tsconfig.json file is set to es6 maybe es7. Until you change the target in tsconfig.json to at least es10 you will not get the real support of JavaScript private fields. I guess you are as curious as me about how the TypeScript compiler transpiles private fields in es6 projects, let's have a look:

Class with # private field before and after transpilation from TypeScript to JavaScript.
Class with "#" private field before and after transpilation from TypeScript to JavaScript.

You can see that tsc in order to mimic the JavaScript mechanics, it creates a WeakMap and stores the value of the private field in it. This approach has a couple of interesting consequences:

  • worst performance,
  • increased memory usage,
  • increased bundle size.

Performance

I was wondering how WeakMaps affect the runtime performance, so I've made a couple of tests on jsben.ch. I was quite surprised with the results.

So I compared which version of JavaScript class is faster (link to the test). The first block of code is from transpilation of a basic class witch uses the hash private and the second one is from a class with a standard private access modifier.

Performance test code
Performance test code

It turned out that creating one thousand new objects is almost four times faster when you use the old method with private class modifiers. In other words, the hash privates have worse performance.

Performance test results.
Performance test results.

Memory consumption

Every private property is transpiled to a separate WeakMap, which increases the memory consumption of your app. In an advanced SPA application, this quickly may grow into a large problem.

Class with three private fields, before and after transpilation.
Class with three private fields, before and after transpilation.

Bundle size

As you can see in the image above this way of transpilation also increases the bundle size, because the WeakMap solution requires more characters. To test it I have created a basic React project using create-react-app (with TypeScript). Then I added a Cat class created with the hash private, I ran the build and did the same for the second implementation. The results have confirmed my thesis. The implementation with private access modifiers produces a smaller bundle size.

Two implementations of Cat class.
Two implementations of Cat class.
Bundle size results.
Bundle size results.

No private scope

The private field is represented as a WeakMap assigned to the global variable. When you copy transpiled code and paste it into the browser console you are able to access a value of the property like in the example below.

Accessing private fields is represented as a global variable.
Accessing private fields is represented as a global variable.

Fortunately, this doesn't apply when it comes to modular JavaScript code, as weak maps don't leak outside of the module.

Parameter Properties

Another issue that I have found with hash privates is that they cannot be used in a constructor. TypeScript has a very elegant way of declaring class fields in a constructor, it's called "Parameter properties".

class Bike {

  constructor(private wheels: number) {}

}

class Car {

  constructor(#engine: string) {} // throws Transpilation Errors

}
Private identifiers cannot be used as parameters.
Private identifiers cannot be used as parameters.

Instead, you need to write it like that:

class Car {

  #engine: string;

  constructor(_engine: string) {
    this.#engine = _engine;
  }
}

One may say this is only the sugar syntax, but in my opinion developer experience is very important and this inconvenience makes hash privates less flexible.

Feel

I spoke with a couple of experienced developers, what is their opinion on "hash" privates and to my surprise, they had similar thoughts to mine. People who have experience in other OOP languages like Java, C++, C#, TypeScript, all felt that the # syntax looks inconsistent. It breaks the commonly used standard of public, protected, private. If you would like to start using it, some of the fields would be public name and the other #description:

class Animal {

  public name: string;

  #type: string;

}

Overall it looks clumsy and confusing, especially for developers who haven't had the opportunity to learn about hash syntax. Also this might look like a combination of two different schools of programming. Maybe it's a matter of time and getting used to it, but at the moment it feels inconsistent and uncomfortable.

IDE

Last but not least, this feature lacks the support of the Webstorm IDE. One of the most simplistic tasks, the variable selection with double click doesn't work.

Webstorm property selection.
Webstorm property selection.

Summary

When you need a real private property e.g. to store some secret data, "hash" private is great for it, because it provides true privacy. Private fields are available only in the scope of the object of a specific class, so nothing leaks outside of it. You get the feeling of the truly encapsulated class object.

Unfortunately, this feature comes with a cost. In other ways, they bring a lot of downsides. I tell my colleagues that:

The whole projects code should look like it is written by only one person.

So when there is a possibility of using two different solutions I always prefer to select one of them for the whole project. This approach simply makes life easier. If you want to decide on one consistent convention, I would recommend using the TypeScript private modifier in favor of the Js one.

Don't get me wrong it's nice to have such a tool in your toolbox. There are cases when it may come in handy. In other cases, I would not recommend using it. It comes with a major downside of a performance drop, memory consumption and also developer experience may suffer.

Luke, Software engineer, passionate about best practices, frameworks React, Vue, patterns and architectures for modern web
				development, open-source creator, TypeScript enthusiast, Generic UI Technical Blog

Luke

Software engineer, passionate about best practices, frameworks React & Vue, patterns and architectures for modern web development, open-source creator, TypeScript enthusiast.