TypeScript encapsulation demystified
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:
As you can see there is no privacy. The private properties after transpilation to JavaScript simply become a public one.
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
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.
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:
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.
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.
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.
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.
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.
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
}
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.
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.