TypeScript 3.8 Finally Fixes Private Fields

The latest release borrows from ES10 to give you the encapsulation you always wanted

One of TypeScript’s best advantages is the way it incorporates new ECMAScript features before they become mainstream. In the past, TypeScript let us use classes, modules, and arrow function syntax from ES6 before mainstream browsers caught up. And now, TypeScript 3.8 (officially released February 21) follows this pattern once again. It supports the emerging private fields standard that’s bundled into ES10 and currently supported by Chrome, Opera, and no one else.

Now, anyone who’s kicked around some code in TypeScript for longer than fifteen minutes knows that TypeScript already supports access modifiers. You can use public (the default), private (to keep one class from messing with another class’s internals), or protected (to allow access in an inherited class — but really, that’s a bit overrated). Access modifiers are programming basics, so having them in TypeScript is a great relief. But they don’t work the way you might expect.

To understand the subtleties of access modifiers and the private fields standard, you need to step back and look at a super-simple TypeScript class. Here’s an example:

class Product {

name: string;

price: number;

description: string; constructor(name: string, price: number) {

this.name = name;

this.price = price;

}

}

There’s nothing special here — it’s one class with three public fields (and one boring constructor). In a serious OOP language like C# or Java, we’d probably replace every public field with a property setter and getter. That’s because in those languages, there’s an expectation that every class has an extra layer of property management. It’s where we plug-in validation, change events, and calculated values. There are also syntactical shortcuts that make properties more streamlined in these languages, and tooling support that makes it more convenient. But in JavaScript and TypeScript, this extra layer of property magic violates expectations. It’s more likely to confuse or surprise developers, which is never a good thing.

That said, we do use properties in TypeScript to handle special cases. For example, let’s say the Product class maps to a database record and — for whatever reason — the database doesn’t accept product names that are longer than 20 characters. This limitation won’t be obvious to developers using the Product class. Worse, setting an invalid product name will cause a late failure during an update, when it’s more difficult to sort out what went wrong. In this scenario, it may be worth adding a property setter to intercept the mistake before it becomes a problem:

class Product {

price: number;

description: string; nameMaxLength = 20; private _name: string;

get name(): string {

return this._name;

}

set name(name: string) {

// Enforce the length limit from the database.

if (name.length > nameMaxLength) {

throw new Error("The maximum Name length is" + nameMaxLength);

}

this._name = name;

} constructor(name: string, price: number) {

this.name = name;

this.price = price;

}

}

Note that this revised Product class does not add getters and setters for the other properties. For example, you could add a price setter to prevent bad code from choosing negative prices, but a negative price obviously stands out as a potential error. Your goal is not to enforce all the rules of your data source (it adds cruft, complicates the code, and violates the expectations of JavaScript). Instead, you add code judiciously to prevent likely problems.

Looking at the revised Product class, it seems like TypeScript has you covered. The private keyword makes _name inaccessible (the underscore prefix is a common but not universal naming convention). But how safe are you?

Not very safe. It turns out that TypeScript’s private keyword is an indication of the class-builder’s intent, but not a true defense against abuse. The private keyword only works when your design tools enforce it. Once you compile your TypeScript to JavaScript, the access keywords and stripped away and everything becomes public. That means if you’re creating a library to be used by other people, there’s nothing to stop them from reaching directly into your previously private fields and mucking about with your class internals.

But wait — it gets worse. It’s true that if you have the TypeScript file open in a tool like VS Code, and you attempt to use _name outside of the Product class, you’ll get squiggly underlines showing your crime. But although these actions are flagged, they don’t translate into compile-time errors. Run your file through the tsc compiler and you’ll get an explanation of the problem, but your JavaScript will still be compiled with the illegal code. And at runtime, that code will successfully access _name .

The private accessibility keyword is a bit of polite documentation. It has no enforcement power.

This is where the emerging private fields standard differs. With private fields, JavaScript enforces the restriction. You simply add a # character before the field you want to make private. That’s it! Here’s a rewritten version of the Product class that uses private fields:

class Product {

price: number;

description: string; nameMaxLength = 20; #name: string;

get name(): string {

return this.#name;

}

set name(name: string) {

// Enforce the length limit from the database.

if (name.length > nameMaxLength) {

throw new Error("The maximum Name length is" + nameMaxLength);

}

this.#name = name;

} constructor(name: string, price: number) {

this.name = name;

this.price = price;

}

}

The code looks similar to the previous example, but the practical reality is very different. Now there’s no way for code outside Product to break the rules and modify _name . Callers won’t even know that the _name field exists.

This solution seems perfect, but there’s one obvious gap. If private fields aren’t supported by most browsers, how can TypeScript enforce them? Happily, the creators of TypeScript have a way to implement private fields that doesn’t depend on direct ES10 support. Instead, TypeScript uses the WeakMaps feature introduced in ES6. (As a side effect, it means that TypeScript’s implementation of the private field feature requires ES6 support, which is a reasonable bar to set.)

You might wonder why TypeScript 3.8 doesn’t just turn private member variables into ES10 private fields automatically during compilation. That approach sounds nice (and it matches our expectation of how private variables should work). But the potential problems are obvious. A language can’t afford to change even minor details of its behavior. Somewhere out there, some lonely coder is depending on the ability to modify supposedly private TypeScript fields. And if you don’t want any part of that nonsense, it’s high time you stepped up to TypeScript 3.8.