TypeScript: ECMAScript Private Fields for Hard Privacy in Classes

This month on February 20th, Microsoft announced the final release of TypeScript 3.8. It has a bunch of new features. One interesting feature is the support for the ECMAScript private fields that are described in this proposal.

With private fields, you get encapsulation that is natively supported in JavaScript. Look at the Friend class below. It has a #firstName private field. You create a private field by starting the name with a hash (#), that’s it. In the constructor of the Friend class the private field #firstName is set to the firstName constructor parameter. Then there’s a getFullName method that returns the value of that #firstName private field.

class Friend {
    #firstName: string;

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

    getFullName() {
        return this.#firstName;
    }
}

When you use the Friend class, it works as expected. You can access the getFullName method, but not the #firstName field, as it is a private field.

let friend = new Friend('Thomas');

console.log(friend.getFullName()); // Works

console.log(friend.#firstName); // Does not work, as it is a private field

The maximum target of the TypeScript 3.8 compiler is ES2020. But private fields are a stage 3 proposal, which means they didn’t make it into the ES2020 standard. That means with TypeScript 3.8 you need to set the compiler target to ESNEXT to see private fields in the generated JavaScript code.

When you set the target of the compiler to ESNEXT, the TypeScript compiler generates this JavaScript code for the Friend class:

class Friend {
    constructor(firstName) {
        this.#firstName = firstName;
    }
    #firstName;
    getFullName() {
        return this.#firstName;
    }
}

Yes, that’s above is JavaScript, and as you can see, private fields are natively supported in JavaScript. That means even if you use the Friend class in pure JavaScript, you’ll get an error if you try to access the #firstName field from outside of the Friend class like below:

console.log(friend.#firstName); // Does also not work in pure JavaScript

TypeScript supports downleveling of private fields. If you use private fields, you need to specify as a minimum target ES2015. With something between ES2015 and ES2020 specified as a target, the TypeScript compiler generates this JavaScript output for the Friend class:

"use strict";
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) {
    if (!privateMap.has(receiver)) {
        throw new TypeError("attempted to set private field on non-instance");
    }
    privateMap.set(receiver, value);
    return value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) {
    if (!privateMap.has(receiver)) {
        throw new TypeError("attempted to get private field on non-instance");
    }
    return privateMap.get(receiver);
};
var _firstName;
class Friend {
    constructor(firstName) {
        _firstName.set(this, void 0);
        __classPrivateFieldSet(this, _firstName, firstName);
    }
    getFullName() {
        return __classPrivateFieldGet(this, _firstName);
    }
}
_firstName = new WeakMap();

As you can see in the last line, a WeakMap is used for the private firstName field.

But wait… I can create a property with a private access modifier. Why private fields?

Great question. The access modifiers private, protected and public are a TypeScript thing, they’re not part of JavaScript. In TypeScript, you can build the Friend class also with a private firstName property like this:

class Friend {
    private firstName: string;

    constructor(firstName: string) {
        this.firstName = firstName;
    }

    getFullName() {
        return this.firstName;
    }
}

As long as you stay in TypeScript, you’re fine, as you’re getting errors when you access the private firstName property from outside of the Friend class:

let friend = new Friend('Thomas');
console.log(friend.firstName); // TypeScript gives you an error here,
                               // as it is a private property

But now let’s look at the JavaScript output. Let’s compile with ES2015 or higher, as ES2015 has support for classes in JavaScript. With ES2015 or higher, you get this class in JavaScript:

class Friend {
    constructor(firstName) {
        this.firstName = firstName;
    }
    getFullName() {
        return this.firstName;
    }
}

Now the important point: If you use the access modifiers public or protected for the firstName property in TypeScript, the JavaScript class above will look exactly the same. Or, let’s say it in other words: In JavaScript, the firstName property is always public. That means the following code is fine in JavaScript:

let friend = new Friend('Thomas');
console.log(friend.firstName); // Works in JavaScript like a charm

That’s the big difference between an ECMAScript private field and a property that is using TypeScript’s private access modifier: ECMAScript private fields are a native JavaScript feature, which means even pure JavaScript will give you an error if you try to access a private field from outside of the class. Not being able to access that field from JavaScript is also known as hard privacy.

Now, when you define such a private field in TypeScript, you can’t set an access modifier like public or private on it, as it is always private.

Microsoft mentioned in the TypeScript 3.8 announcement some of the most important rules of private fields:

Private fields start with a # character. Sometimes we call these private names.

Every private field name is uniquely scoped to its containing class.

TypeScript accessibility modifiers like public or private can’t be used on private fields.

Private fields can’t be accessed or even detected outside of the containing class – even by JavaScript users! Sometimes we call this hard privacy.

https://devblogs.microsoft.com/typescript/announcing-typescript-3-8/#ecmascript-private-fields

What should you use: private fields or properties with private access modifier?

It depends! Private fields mean that no user of your code can access the field. If you create a normal property with private access modifier, the property is accessible in JavaScript from outside of the class, which could be a problem, but it could also be useful in some situations.

If you want real encapsulation and hard privacy, you might want to go with private fields. But before you do that, there’s more to consider.

For private fields, you need to target ES2015 or higher

If you use private fields in TypeScript, you need to target ES2015 or higher. But with a property with a private access modifier you can target even the lowest version, which is ES3.

Private fields are a bit slower

If performance is important, be aware that private fields are downleveled with WeakMaps. And right now, WeakMaps are not as fast as pure properties.

Wrap up

Overall, I think private fields are a great feature of ECMAScript, and in TypeScript, you can use them already today. But consider the thoughts above.

Happy coding,
Thomas

Share this post

Leave a Reply

Your email address will not be published.

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.