Skip to main content
  1. Blog/

TypeScript 3.8 Beta — Private Fields and Top-Level Await Land

·1123 words·6 mins
Osmond van Hemert
Author
Osmond van Hemert
JavaScript & Node.js - This article is part of a series.
Part : This Article

The TypeScript team announced the 3.8 beta this week, and it’s a release that I think deserves more attention than the version number suggests. While it doesn’t have the headline-grabbing appeal of optional chaining (that was 3.7), the features in 3.8 signal important shifts in both TypeScript and the broader JavaScript ecosystem.

The two headliners: ECMAScript private fields and top-level await. Both are features that change how you structure code in meaningful ways.

ECMAScript Private Fields — Finally, Real Privacy
#

TypeScript has had the private keyword since day one. But here’s the thing that catches people off guard: TypeScript’s private is a compile-time-only concept. At runtime, the property is completely accessible. It’s a gentlemen’s agreement enforced by the type checker, not by the JavaScript engine.

ECMAScript private fields, using the # prefix syntax, are different. They’re truly private at runtime:

class Person {
  #name: string;

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

  greet() {
    console.log(`Hello, I'm ${this.#name}`);
  }
}

const p = new Person("Osmond");
p.#name; // Error! Property '#name' is not accessible
          // outside class 'Person' because it has a
          // private identifier.

The #name field literally doesn’t exist from outside the class. You can’t access it with bracket notation, you can’t find it with Object.keys(), and you can’t reach it through the prototype chain. This is enforcement by the JavaScript engine, not the type checker.

Now, I can already hear the pragmatists asking: “Why do I need this when TypeScript’s private keyword works fine?” Fair question. Here’s why it matters:

  1. Library authors: If you publish a library, your private members are accessible to consumers who use JavaScript directly (or who use // @ts-ignore). True private fields mean your internal implementation details are genuinely hidden.

  2. Security-sensitive code: In scenarios where you’re handling credentials, tokens, or sensitive state, compile-time privacy isn’t sufficient. # fields prevent any runtime inspection.

  3. Future-proofing: This is the direction ECMAScript is going. The class fields proposal is at Stage 3, which means it’s essentially locked in for JavaScript. TypeScript is aligning with the language it compiles to.

That said, there are practical concerns. The # syntax is polarizing — it looks foreign to developers coming from Java, C#, or even TypeScript’s own private keyword. There will be a period where codebases have a mix of private and # fields, which could be confusing.

My recommendation: for new code in libraries that are consumed externally, prefer # fields. For application code where everything goes through the TypeScript compiler, private is still perfectly fine.

Top-Level Await — A Bigger Deal Than It Looks
#

The second major feature is support for top-level await. Currently, you can only use await inside an async function:

// Before: awkward IIFE wrapper
(async () => {
  const data = await fetch("/api/config");
  const config = await data.json();
  // use config...
})();

With top-level await, you can write this at the module level:

// After: clean and direct
const data = await fetch("/api/config");
const config = await data.json();
export { config };

This is particularly valuable for:

  • Module initialization: Modules that need to load configuration, establish database connections, or perform other async setup can now do so cleanly.
  • Scripts and tooling: CLI tools and scripts that are essentially one big async operation no longer need the IIFE dance.
  • Dynamic imports: You can await import("./module") at the top level, enabling conditional module loading patterns.

The catch — and it’s an important one — is that top-level await only works in ES modules, not CommonJS. TypeScript enforces this by requiring "module": "esnext" or "module": "system" in your tsconfig.json. If you’re still shipping CommonJS (which many Node.js projects are), you’ll need to evaluate whether the module system switch is worth it.

There’s also a subtlety around execution order. When module A uses top-level await, any module that imports from A will wait for that await to resolve before executing. This creates implicit asynchronous dependencies in your module graph, which can affect startup performance if you’re not careful.

Export * As Namespace
#

The third feature is less dramatic but solves a real annoyance:

export * as utilities from "./utilities";

This re-exports everything from ./utilities as a namespace object. Previously, you had to do this in two steps:

import * as utilities from "./utilities";
export { utilities };

It’s a small quality-of-life improvement that makes barrel files (index.ts re-export modules) cleaner. If you maintain libraries with complex module structures, you’ll appreciate this.

Type-Only Imports and Exports
#

Perhaps the most practically useful feature for day-to-day development is the new import type syntax:

import type { SomeType } from "./module";

This explicitly marks an import as type-only, meaning it will be completely erased during compilation. No runtime import, no side effects, no bundle size impact.

This solves a genuine problem. With regular imports, TypeScript has to figure out whether you’re using the import as a value (which needs to stay in the emitted JavaScript) or only as a type (which can be erased). In most cases it gets this right, but there are edge cases — particularly with re-exports and isolatedModules mode — where the ambiguity causes issues.

With import type, the intent is explicit. I expect this to become a best practice, especially in projects using bundlers where tree-shaking depends on clean import analysis.

The Bigger Picture
#

What I find interesting about TypeScript 3.8 is how much of it is about alignment with ECMAScript proposals. Private fields, top-level await, and export * as namespace are all TC39 proposals that TypeScript is implementing. This is TypeScript doing what it does best: giving you early access to future JavaScript features with type safety bolted on.

The pace of TypeScript releases continues to be impressive. We’ve gotten optional chaining, nullish coalescing, and now private fields and top-level await — all within a few months. The TypeScript team’s ability to ship meaningful features on a regular cadence while maintaining backward compatibility is, frankly, the gold standard for language evolution. The Python 2/3 situation this language is not.

My Take
#

TypeScript 3.8 is a solid, pragmatic release. The features aren’t revolutionary on their own, but they collectively make TypeScript a more complete and more aligned superset of JavaScript.

If I had to pick one feature to adopt immediately, it would be import type. It costs nothing, makes your intent explicit, and prevents a class of subtle bundling issues. Private fields with # are great for library authors but can wait for broader ecosystem adoption. Top-level await is powerful but requires careful thought about your module system.

The TypeScript team continues to be one of the best examples of language stewardship in the industry. They ship often, break almost nothing, and consistently make developers’ lives better. That’s not a bad way to start the decade.

JavaScript & Node.js - This article is part of a series.
Part : This Article