In this post you’ll learn all about private properties and methods in JavaScript using the new #
syntax.
The Public and Private Instance Fields Proposal is currently at Stage 3 – close to completion. Also check out the class field examples on TC39’s GitHub.
This proposal introduces a brand new syntax that finally gives us private properties and methods in JavaScript. As the name suggests, we’ll be looking at instance properties and methods – meaning objects created using the new
keyword.
Before this syntax was introduced, JavaScript didn’t have (and actually, still doesn’t have unless we adopt Babel) true private properties and methods.
This lack of feature led to emulating private properties and methods by using an underscore-prefix:
function User(name) {
this._id = 'xyz';
this.name = name;
}
User.prototype.getUserId = function () {
return this._id;
}
User.prototype._destroy = function () {
this._id = null;
};
const user = new User('Mike Watson');
user._id; // xyz
user.getUserId(); // xyz
user._destroy();
user.getUserId(); // null
Even though this._id
and User.prototype._destroy
were intended to be private, this prefixing doesn’t stop anyone from accessing any of the properties as they are part of the User
object.
Above we’re calling user._destroy()
when really it is considered private and could change at any time, so users should not use or rely on our private properties or methods.
Now with the introduction of the class
keyword, we’ve now arrived at a Stage 3 proposal – we’re almost there! So what does it look like?
Let’s switch our .prototype
approach over to a class
and go from there.
Constructor Functions to Classes
With the introduction of the class
keyword, fast-forwarding to today gives us an equivalent of the previous .prototype
example:
class User {
constructor(name) {
this._id = 'xyz';
this.name = name;
}
getUserId() {
return this._id;
}
_destroy() {
this._id = null;
}
}
const user = new User('Mike Watson');
user._id; // xyz
user.getUserId(); // xyz
user._destroy();
user.getUserId(); // null
But the problem still remains. However, this new feature is only available with a class
, hence the switch.
📣 Note: class
is syntax sugar and is not something fundamentally different from prototypes. A class
in most cases is compiled down to ES5 constructor functions and properties and methods are translated onto the prototype
!
Private Properties in Classes
Now we’ve got our class setup, let’s make the _id
property private property using #
:
class User {
#id = 'xyz';
constructor(name) {
this.name = name;
}
getUserId() {
return this.#id;
}
}
Something is also different above, we’ve declared #id = 'xyz';
on one-line above the constructor! This is called property initilizer syntax and is the reason we’ll be using @babel/plugin-proposal-class-properties (I’ll show you how to set this stuff up at the end of this post too).
You could also do this and declare the private property as undefined
and then assign inside the constructor
:
class User {
#id;
constructor(name) {
this.name = name;
this.#id = 'xyz';
}
getUserId() {
return this.#id;
}
}
It’s required to declare the private property #id;
you’re creating on the class itself otherwise you’ll get an error such as Private name #id is not defined
.
We can only reference the #id
property inside the class, any public property access would just be undefined
:
const user = new User('Mike Watson');
user.id; // undefined
user.getUserId(); // xyz
Now we’ve grasped private properties, let’s move onto private methods!
Private Methods in Classes
First off, before we look at private methods, there’s a super easy way involving a private property and an arrow function (so we’re kinda cheating by calling it a method… but it looks and behaves like one):
class User {
#id = 'xyz'
constructor(name) {
this.name = name;
}
getUserId() {
return this.#id;
}
#destroy = () => {
this.#id = null;
};
}
As #destroy
is actually a private property with an arrow function assigned, our setup would work out of the box. However, it would not be translated into a prototype method. Our this
context is correct though, so you could totally just use arrow functions – but we lose the benefit of using the prototype
and sharing the methods throughout multiple instances, instead with each new
call they would be constructed again.
Really though, we want to do it the proper way and use a method, which would be transformed into User.prototype.destroy = function () {}
if it wasn’t private, our getUserId() {}
method would live on the prototype
instead:
class User {
#id = 'xyz';
constructor(name) {
this.name = name;
}
getUserId() {
return this.#id;
}
#destroy() {
this.#id = null;
}
}
With ESLint this turned out to be a bit more of a headache than I’d anticipated, as running the code gave me this:
❌ error ‘destroy’ is not defined no-undef
I went through a rabbit-hole of GitHub issues (this and this) to arrive at a solution to using the new feature:
// eslint-disable-next-line no-undef
#destroy() {
this._id = null;
}
Basically forcing the no-undef
error to silence. You can then use this.#destroy()
anywhere inside the class after this with no issues – the code compiles down perfectly, giving us this object:
User { name: 'Todd Motto', getUserId: ƒ }
As promised, I’ll show you how to set it up with Babel and you can even download the source project.
Using Private Properties and Methods with Babel
First we’ll need to consider the following:
Install them all:
npm install --save-dev @babel/plugin-proposal-class-properties @babel/plugin-proposal-private-methods babel-eslint
Once we’ve installed the above packages, we’ll need to change our .eslintrc.json
to:
//.eslintrc.json
{
"parser": "babel-eslint"
}
This uses Babel’s ESLint parser over ESLint directly to give us finer control.
Next, we then need a .babelrc
that looks like:
// .babelrc
{
"presets": ["@babel/preset-env"],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-private-methods"
]
}
You’re good to go, now you can go and use private properties and methods properly in JavaScript for the first time.
Summary
The way we write JavaScript can now give us true private properties and methods.
Through the Babel compiler webpack handles our modules under-the-hood to scope our JavaScript in a far better way than the this._id
prefix approach – it hides the property, or method, altogether.
When the feature lands, we’ll have true private properties and methods in JavaScript, until then it’s compiling with Babel (which is a sensible approach anyway).