· 5 min read ·

TypeScript 6.0 Draws the Line Between Types and Code

Source: typescript

TypeScript just landed its sixth major version, and if you have been following the language’s trajectory over the past two years, the timing feels right. The announcement describes it in familiar terms, but the version bump carries weight. TypeScript has crossed major version boundaries only five times before, and each crossing meant something: 2.0 brought strict null checks, 4.0 brought variadic tuple types, 5.0 formalized decorators via the ECMAScript standard. Version 6.0 follows that pattern, but the nature of the change is more architectural than syntactic.

The throughline in TypeScript 6.0 is the distinction between erasable and non-erasable TypeScript syntax, and what that distinction means for the language’s relationship with the broader JavaScript ecosystem.

The Two Kinds of TypeScript Syntax

Most TypeScript syntax is erasable. Type annotations, interface declarations, generic parameters, as expressions, non-null assertions: all of these vanish when TypeScript compiles to JavaScript. The output is structurally identical to the input; only the type information is gone. A function like function add(x: number, y: number): number { return x + y; } becomes function add(x, y) { return x + y; }. Strip the annotations and you have valid JavaScript.

But several TypeScript constructs require code generation to function. They cannot be stripped; they must be transformed.

Enums generate an object and, for non-const numeric enums, a reverse mapping:

enum Status { Pending, Active, Closed }
// Emits:
var Status;
(function (Status) {
    Status[Status["Pending"] = 0] = "Pending";
    Status[Status["Active"] = 1] = "Active";
    Status[Status["Closed"] = 2] = "Closed";
})(Status || (Status = {}));

Namespaces generate immediately invoked function expressions:

namespace Utils { export function log(msg: string) { console.log(msg); } }
// Emits:
var Utils;
(function (Utils) {
    function log(msg) { console.log(msg); }
    Utils.log = log;
})(Utils || (Utils = {}));

Parameter properties generate assignments inside constructor bodies:

class Config { constructor(public host: string, public port: number) {} }
// Emits:
class Config { constructor(host, port) { this.host = host; this.port = port; } }

These features have been part of TypeScript since its early years, when the language’s job was to provide both type-checking and syntactic sugar that generated clean ES5. That was a reasonable design in 2013. By 2025, the landscape had changed considerably.

Why Erasability Matters Now

Node.js 22.6, released in June 2024, shipped experimental TypeScript stripping support. The implementation uses Amaro, a wrapper around the SWC parser, to strip type annotations from TypeScript files before executing them. It does not run the TypeScript compiler. It does not check types. It does not transform enums, namespaces, or parameter properties. It strips, and nothing more.

This works because stripping is fast and simple: find the type annotations, remove them, execute the JavaScript that remains. But the “JavaScript that remains” is only valid if the original TypeScript was written in the erasable subset. If you have enum Direction { Up, Down } in your source file, stripping the types leaves behind enum Direction { Up, Down }, which is not valid JavaScript. The stripper fails, or worse, silently mishandles it.

Deno and Bun take the same approach. Both support TypeScript natively by stripping types rather than compiling them. Both depend on the erasable subset for correct behavior. And the TC39 type annotations proposal, which is working its way through the standards process, formalizes this at the language specification level. If the proposal advances, JavaScript engines could natively ignore type annotations without any transformation step. The proposal only covers erasable syntax. Enums and namespaces cannot be “ignored”; they carry runtime semantics that require code generation.

TypeScript 6.0 meets this ecosystem reality with the --erasableSyntaxOnly compiler option, which rejects any TypeScript syntax that requires code generation. Enabling it turns enums, namespaces, and parameter properties into compiler errors. If your project compiles cleanly with this flag, it is compatible with Node’s native stripping, Deno, Bun, and any future engine that implements type annotations natively.

Breaking Changes and Scope Reduction

TypeScript 6.0 also removes several compiler options that have been deprecated through the 5.x series. The --target ES3 and --target ES5 output modes are gone. TypeScript no longer polyfills or downgrades JavaScript features to support environments from 2009. That job belongs to dedicated transpilers like Babel or swc. The --out option, which concatenated compiled output into a single file using a pre-module mechanism, is also gone. These options date from TypeScript’s era as a comprehensive JavaScript compilation toolchain.

Dropping support for running the TypeScript compiler on Node.js versions below 18 is part of the same story. The compiler itself now requires a reasonably modern runtime, which enables it to use modern Node APIs and reduces the maintenance surface across cross-version compatibility shims.

Taken together, these removals narrow the scope of what TypeScript does. The language is shedding the parts of its surface area that exist to compensate for an ecosystem that no longer needs compensating.

Migrating Away from Non-Erasable Syntax

For projects that want to adopt --erasableSyntaxOnly, the migration paths are well-established. Enums convert cleanly to as const objects:

// Before
enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT" }

// After
const Direction = {
  Up: "UP",
  Down: "DOWN",
  Left: "LEFT",
  Right: "RIGHT",
} as const;
type Direction = typeof Direction[keyof typeof Direction];

The static type guarantees are equivalent. The only loss is the reverse mapping on numeric enums, which most codebases do not rely on. Numeric enums have carried a long-standing type safety issue: TypeScript accepts let s: Status = 999 without error for any numeric enum, which the as const pattern avoids by deriving a union of literal types instead.

Parameter properties convert to explicit declarations:

// Before
class Server { constructor(public host: string, public port: number) {} }

// After
class Server {
  host: string;
  port: number;
  constructor(host: string, port: number) {
    this.host = host;
    this.port = port;
  }
}

More verbose, but unambiguous to any TypeScript toolchain regardless of how it processes the source. Namespaces convert to ES modules, which they should have been for any code targeting a modern module system.

What This Release Signals

TypeScript 6.0 is not primarily a type-system release. It does not introduce a new category of type reasoning or a novel kind of inference. TypeScript’s code-generation capabilities served a genuine purpose when the language launched, in an era before ES modules, before native class syntax, before any of the ECMAScript features TypeScript anticipated. Those capabilities accumulated into a surface area that is now more liability than asset.

The JavaScript ecosystem has converged on a consistent position: TypeScript syntax should be erasable. Node.js, Deno, Bun, and the TC39 proposal all reflect that convergence. TypeScript 6.0, through --erasableSyntaxOnly and the removal of legacy compilation targets, reflects it too.

Major version bumps in TypeScript are rare enough that they tend to mark genuine inflection points. This one marks the language’s alignment with a JavaScript ecosystem that has grown up around it. The runtimes handle TypeScript natively, the standards body is formalizing type annotations as a language concept, and the tools that process TypeScript at scale do so by stripping rather than compiling. TypeScript 6.0 is the version that fits into that ecosystem most cleanly, not by adding new machinery, but by clarifying that type-checking was always the point.

Was this interesting?