· 7 min read ·

TypeScript 6.0 and the Case for Erasable Syntax

Source: typescript

Major version numbers in TypeScript carry weight. The 4.0 release brought variadic tuple types and labeled tuples in 2020. The 5.0 release in 2023 landed decorators aligned to the TC39 spec, const type parameters, and the beginning of a serious performance push across the 5.x series. The team does not increment the major version for marketing reasons. When TypeScript reaches a new major, something philosophical has shifted.

TypeScript 6.0 arrives in a landscape that looks fundamentally different from the one 5.0 entered. Node.js 22.6 shipped --experimental-strip-types in mid-2024. Node.js 23 made type stripping stable. Deno has run TypeScript files natively since day one. Bun treats TypeScript as a first-class input without any configuration. The premise that TypeScript files must pass through a dedicated compiler step before execution is no longer the universal default. For new projects starting today, it is often not even the recommended path.

TypeScript 6.0 does not ignore this. It builds on it.

What Type-Stripping Actually Requires

Native runtime support for TypeScript works through a simple mechanism: remove all the type annotations and run the resulting JavaScript. No transformation, no code generation, no emit configuration. The TypeScript syntax that looks like const x: number = 5 becomes const x = 5. Function signatures lose their parameter types. Interfaces and type aliases vanish entirely. This is fast, requires no build tooling, and produces JavaScript that is semantically identical to what the developer wrote.

The catch is that this only works for TypeScript constructs that are purely additive annotations on top of valid JavaScript. Several TypeScript features actually generate JavaScript code at emit time, and a type-stripper cannot handle them.

Enums are the most common example:

enum Direction {
  Up,
  Down,
  Left,
  Right
}

This compiles to:

var Direction;
(function (Direction) {
  Direction[Direction["Up"] = 0] = "Up";
  Direction[Direction["Down"] = 1] = "Down";
  Direction[Direction["Left"] = 2] = "Left";
  Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));

There is no way to strip the TypeScript enum declaration and arrive at valid JavaScript. The entire runtime object needs to be emitted. A tool that only strips types cannot produce this output, so files containing enums cannot run through Node’s built-in TypeScript support.

The same problem applies to namespaces, which compile to nested IIFE structures:

namespace Utils {
  export function format(s: string): string {
    return s.trim();
  }
}

And to parameter properties, which generate field assignment code inside constructors:

class User {
  constructor(
    private readonly id: string,
    public name: string
  ) {}
}

The TypeScript compiler emits assignments for this.id = id and this.name = name inside the constructor body. A type-stripper would remove the private readonly and public modifiers but leave nothing behind for the actual field initialization, producing broken JavaScript.

Legacy decorators with emitDecoratorMetadata present the same issue: they cause the compiler to emit __decorate and __metadata calls that have no counterpart in the stripped source.

The —erasableSyntaxOnly Response

TypeScript 5.8 introduced --erasableSyntaxOnly as a direct response to this problem. When the flag is enabled, the compiler errors on any TypeScript-specific construct that cannot be handled by a simple type-stripper. Enums produce an error. Namespaces produce an error. Parameter properties produce an error.

The intent is explicit: if you want your TypeScript files to be runnable by Node 22.6+ without a build step, enable this flag and the compiler will tell you when you have written something that would break that goal.

For enums, the idiomatic replacement under --erasableSyntaxOnly is a const object with as const:

const Direction = {
  Up: 0,
  Down: 1,
  Left: 2,
  Right: 3,
} as const;

type Direction = typeof Direction[keyof typeof Direction];

This is plain JavaScript with a type alias layered on top. A type-stripper produces valid JavaScript from it. The runtime object is identical to what the enum would have generated.

Parameter properties have a similarly direct replacement:

class User {
  readonly id: string;
  name: string;

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

More verbose, but every line is valid JavaScript with type annotations on top.

TypeScript 6.0 elevates the significance of this flag and the patterns around it. The guidance is not just that --erasableSyntaxOnly exists as an option for teams that want native runtime compatibility. It is that writing erasable TypeScript is the idiomatic path for modern codebases, and the constructs that prevent it are legacy features that carry ongoing maintenance cost for the ecosystem.

Isolated Declarations and the Build Performance Story

A second thread running through recent TypeScript releases reaches a milestone with 6.0: --isolatedDeclarations, introduced in TypeScript 5.5.

The problem this solves is fundamental to large monorepos. In a standard TypeScript project, generating .d.ts declaration files requires full type inference across the entire dependency graph. Package B cannot generate its declaration files until package A has finished, because B’s types may depend on inferred types from A. This forces serial type-checking even when you have many parallel build workers available.

--isolatedDeclarations changes the contract. When enabled, TypeScript requires that all exported values have explicit type annotations:

// --isolatedDeclarations: error
export function add(a: number, b: number) {
  return a + b;
}

// --isolatedDeclarations: valid
export function add(a: number, b: number): number {
  return a + b;
}

With explicit return types on all exports, the declaration file for any module can be generated by looking only at that module, without resolving the types of its dependencies. This makes .d.ts generation embarrassingly parallel. Tools like Rollup’s DTS plugin, oxc-transform, and custom bundlers can generate declaration files for every package simultaneously.

The performance gains are not marginal. For a monorepo with 50 packages, the difference between serial and parallel declaration generation can be the difference between a type-check step that takes 30 seconds and one that takes 4. The TypeScript team has benchmarked this extensively, and the results are consistent: annotation-explicit codebases with --isolatedDeclarations scale far better than those relying on inference.

TypeScript 6.0 builds on this foundation. The combination of --isolatedDeclarations and --erasableSyntaxOnly represents a clear picture of what idiomatic TypeScript looks like at scale: explicit annotations, no emit-transforming constructs, and a build process that can be fully parallelized.

Module Resolution and the Node18 Minimum

Like every major TypeScript version, 6.0 raises the minimum supported Node.js version. This follows the TypeScript team’s established pattern of dropping support for Node.js versions that have reached end-of-life. Node.js 16 reached EOL in September 2023, and Node.js 18 reaches EOL in April 2025. TypeScript 6.0 aligns with these lifecycle boundaries.

The module resolution landscape has also matured significantly. The --moduleResolution bundler option introduced in TypeScript 5.0 handles the hybrid ESM/CJS environment that most modern bundlers operate in, and the --module nodenext and --module node16 options provide strict ESM/CJS interop for Node.js environments. TypeScript 6.0 continues to push teams toward these modern resolution modes and away from the legacy --moduleResolution node that predates ESM.

The --rewriteRelativeImportExtensions flag from TypeScript 5.7 is particularly relevant here. When writing ESM TypeScript that targets Node.js or a native runtime, you often need to write import { foo } from './utils.js' even though the actual file on disk is utils.ts. The flag automates the rewriting, making the TypeScript development experience cleaner while producing correct output for ESM runtimes.

What This Costs

None of this is free. The TypeScript codebase has been using enums internally for years, and the TypeScript compiler itself is a large TypeScript project. The team has been working through their own migration from enum-heavy code to const object patterns, and 6.0 represents a point where that migration has progressed enough to make stronger recommendations outward.

For projects that have invested heavily in TypeScript enums, the migration is mechanical but time-consuming. Enums produce a closed set of named values; the as const replacement produces the same runtime behavior but requires slightly more ceremony to get proper type narrowing. The community has developed reliable patterns for this, and automated codemods exist to handle most cases, but the work is real.

Namespaces are less common in modern codebases. They were a TypeScript-ism from an era before ES modules, and most teams moved away from them years ago when ESM support matured. Parameter properties are trickier because they are genuinely convenient, and the verbosity of the explicit alternative is noticeable in constructor-heavy code. The trade-off is clear, though: native runtime compatibility and parallelizable type-checking in exchange for more explicit constructors.

The legacy decorator situation is separate from this. The TC39 decorator proposal that TypeScript 5.0 adopted does not require emitDecoratorMetadata, and the new decorators are erasable in the sense that the decorator syntax itself strips cleanly. The metadata emission from the old experimentalDecorators mode is what causes problems, and that mode has been superseded by the standardized implementation.

The Broader Shift

TypeScript started as a transpiler with a type checker attached. The value proposition was: write modern JavaScript with types, compile to whatever JavaScript your runtime supports, catch errors at compile time. That story made sense in 2012, when targeting ES5 was mandatory and the TypeScript compiler was also handling downleveling of ES6+ features.

In 2026, V8, SpiderMonkey, and JavaScriptCore all support modern JavaScript natively. Downleveling is rarely needed. What TypeScript actually provides is the type system. The transpiler part has been gradually becoming an optional component, and TypeScript 6.0 accelerates that transition by treating native runtime compatibility as a design constraint rather than a bonus feature.

This is a good direction. The complexity of TypeScript build configurations, the confusion around moduleResolution options, and the friction of keeping tsconfig.json aligned with bundler behavior all trace back to TypeScript historically trying to be both the type checker and the build tool. Separating those concerns, with TypeScript owning the type layer and native runtimes handling execution, simplifies both sides.

The tools that power modern TypeScript development, esbuild, swc, oxc, have always worked this way. They strip types without running the type checker, relying on a separate tsc --noEmit pass for type validation. TypeScript 6.0 makes the language itself a better fit for this architecture.

Was this interesting?