JavaScript private class fields and the TypeScript private modifier

In this post we'll shed light on ECMAScript private fields and see how they compare to the TypeScript private modifier.

JavaScript private class fields

JavaScript private class fields and the need for privacy

A closure is the only JavaScript native mechanism for protecting variables from access.

Closures are the foundation for a lot of private-like patterns, like the popular module pattern. But after ECMAScript 2015 classes took over in recent years, developers felt the need for more control over classes member privacy.

The class field proposal (at the time of writing in stage 3) tries to solve the problem with the introduction of private class fields.

Let's see how they look like.

JavaScript private class fields, an example

Here's a JavaScript class with private fields, note that unlike "public" members every private field must be declared before access:

class Person {
  #age;
  #name;
  #surname;

  constructor(name, surname, age) {
    this.#name = name;
    this.#surname = surname;
    this.#age = age;
  }

  getFullName() {
    return `${this.#name} + ${this.#surname}`;
  }
}

Private class fields are not accessible from outside the class:

class Person {
  #age;
  #name;
  #surname;

  constructor(name, surname, age) {
    this.#name = name;
    this.#surname = surname;
    this.#age = age;
  }

  getFullName() {
    return `${this.#name} + ${this.#surname}`;
  }
}

const marta = new Person("Marta", "Cantrell", 33);
console.log(marta.#age); // SyntaxError

This is true "privacy". At this point if you now a bit of TypeScript you might ask what "native" private fields have in common with the private modifier in TypeScript.

Well, the answer is: nothing. But why?

The private modifier in TypeScript

The private modifier in TypeScript should be familiar to developers coming from more traditional backgrounds. In brief, the keyword is meant to deny class members access from outside the class.

But let's not forget, TypeScript is a layer on top of JavaScript and the TypeScript compiler is supposed to strip away any fancy TypeScript annotation, including private.

That means the following class doesn't do what you think it does:

class Person {
  private age: number;
  private name: string;
  private surname: string;

  constructor(name: string, surname: string, age: number) {
    this.name = name;
    this.surname = surname;
    this.age = age;
  }

  getFullName() {
    return `${this.name} + ${this.surname}`;
  }
}

const liz = new Person("Liz", "Cantrill", 31);
// @ts-ignore
console.log(liz.age);

Without // @ts-ignore, accessing liz.age throws an error only in TypeScript, yet after the compilation you end up with the following JavaScript code:

"use strict";
var Person = /** @class */ (function () {
    function Person(name, surname, age) {
        this.name = name;
        this.surname = surname;
        this.age = age;
    }
    Person.prototype.getFullName = function () {
        return this.name + " + " + this.surname;
    };
    return Person;
}());

var liz = new Person("Liz", "Cantrill", 31);
console.log(liz.age); // 31

As expected we're free to print Liz's age. The main take here is that private in TypeScript is not so private, and it feels convenient only at the TypeScript level, not for "real privacy".

And now let's get to the point: "native" private class fields in TypeScript.

Private class fields in TypeScript

TypeScript 3.8 added support for ECMAScript private fields, not to be confused with the TypeScript private modifier.

Here's a class with private class fields in TypeScript:

class Person {
    #age: number;
    #name: string;
    #surname: string;

    constructor(name:string, surname:string, age:number) {
        this.#name = name;
        this.#surname = surname;
        this.#age = age;
    }

    getFullName() {
        return `${this.#name} + ${this.#surname}`;
    }
}

Not so different from vanilla JavaScript, besides type annotations. Members cannot be accessed from the outside. But the real problem with private fields in TypeScript is that they use WeakMap under the hood.

To compile this code we need to adjust the target compilation version in tsconfig.json, which must be at least ECMAScript 2015:

{
  "compilerOptions": {
    "target": "es2015",
    "strict": true,
    "lib": ["dom","es2015"]
  }
}

This could be a problem depending on the target browser, unless you intend to ship a polyfill for WeakMap, which at that point becomes too much work if it's just for the sake of writing fancy new syntax.

There is always this tension in JavaScript, where you really want to use the new syntax, but on the other hand you don't want to let the UX down with a gazillion polyfills.

On the flip side I don't think you should worry too much about private class fields, even if you want to ship to newer browsers. At least for now. The support for private fields is almost non-existent. Not even Firefox has implemented the proposal.

Let's see what the future holds.

Conclusions

Still a proposal at the time of writing, JavaScript class fields are interesting, but the support among browser vendors is poor. What's your take on this feature?

Here's mine:

  • I like ES private class fields (though I dislike the #)
  • I'll wait until private class fields land in all major browsers
  • I wouldn't use private class fields in TS today because of WeakMap
  • private in TypeScript seems a better choice, but works only at the static level

The official announcement for TypeScript 3.8 private fields.

Valentino Gagliardi

Hi! I'm Valentino! I'm a freelance consultant with a wealth of experience in the IT industry. I spent the last years as a frontend consultant, providing advice and help, coaching and training on JavaScript, testing, and software development. Let's get in touch!