· 7 min read ·

TypeScript 6.0 and the Long Arc Toward Erasable Code

Source: typescript

The TypeScript 6.0 announcement lands against a very different backdrop than any previous major version. When TypeScript 4.0 shipped in 2020, the language was primarily a compile step. You wrote TypeScript, you ran tsc, you got JavaScript. That model is still valid today, but it is no longer the only one, and TypeScript 6.0 reflects how much has shifted around it.

A Brief History of TypeScript’s Version Numbers

TypeScript 1.0 shipped in April 2014. The language has followed a roughly two-to-three year cadence for major version bumps: 2.0 in 2016, 3.0 in 2018, 4.0 in 2020, 5.0 in 2023. Each major version brought real breaking changes and signaled a philosophical shift. TypeScript 2.0 gave us strict null checks. TypeScript 3.0 introduced project references. TypeScript 4.0 reworked tuple types and variadic generics. TypeScript 5.0 deprecated legacy configuration options and began pruning historical baggage.

TypeScript 6.0 continues that pruning, and it does so in ways that reflect a question the community has been circling for several years: what should TypeScript transform, versus what should it merely annotate?

The Native Stripping Shift

The catalyst for this question is not academic. In 2024, Node.js 22.6 shipped experimental support for running TypeScript files directly via --experimental-strip-types. This uses amaro, a thin wrapper around SWC, to remove type annotations before execution. The constraint is fundamental: the stripper can only remove syntax. It cannot transform syntax. If your TypeScript code uses enum, namespace, or parameter properties, native stripping fails outright.

Deno has supported TypeScript natively since version 1.0 in 2020, using a similar approach. Bun compiles TypeScript on the fly with its own parser. The common thread is that all three runtimes treat TypeScript types as annotations to be discarded, not features to be compiled into something else.

This puts pressure on TypeScript to be clear about what it calls “erasable” syntax, meaning syntax that can be stripped to yield valid JavaScript, versus “transforming” syntax that actually generates code that was never in the source.

Erasable vs. Transforming: The Core Distinction

Most TypeScript syntax is erasable. Type annotations, interfaces, type aliases, generic parameters, as casts, satisfies expressions, and declare statements all vanish when you strip types, leaving perfectly valid JavaScript. The runtime behavior of the resulting code is identical to what you would write without TypeScript.

But several TypeScript features are not erasable:

// Enums compile to an IIFE that creates a real JS object
enum Status {
  Active,
  Inactive,
  Pending
}
// Emits:
// var Status;
// (function (Status) {
//   Status[Status["Active"] = 0] = "Active";
//   ...
// })(Status || (Status = {}));

// Parameter properties generate field assignments in the constructor
class User {
  constructor(public name: string, private id: number) {}
}
// The fields and assignments are synthesized—they did not exist in the source

// Legacy namespaces compile to IIFE wrappers
namespace Auth {
  export function verify(token: string): boolean { return true; }
}

None of these can be meaningfully stripped. They emit JavaScript that did not exist in the original source file. A stripper that encounters an enum declaration has two options: error out or produce broken JavaScript.

The --erasableSyntaxOnly Flag

TypeScript introduced the --erasableSyntaxOnly compiler option as a way for projects to enforce this boundary. With this flag enabled, the compiler will raise an error if it encounters any non-erasable syntax:

{
  "compilerOptions": {
    "erasableSyntaxOnly": true,
    "module": "nodenext",
    "target": "esnext"
  }
}

Attempting to use an enum with this flag active produces a type error immediately. The same applies to parameter properties and legacy namespace declarations. This is the flag you want if you intend for your TypeScript to be directly runnable via Node.js’s native stripping, or if you want to ensure portability across runtimes without a dedicated build step.

TypeScript 6.0 formalizes this distinction and makes --erasableSyntaxOnly a more prominent recommendation for new projects. It is the clearest statement yet that TypeScript considers the “just erase the types” model a first-class use case rather than an edge case.

What To Do About Enums

Enums are the most common source of non-erasable syntax in existing TypeScript codebases. The good news is that modern TypeScript has idiomatic replacements.

The canonical alternative is a const object combined with a derived type:

// Instead of:
enum Status {
  Active = "active",
  Inactive = "inactive"
}

// Use:
const Status = {
  Active: "active",
  Inactive: "inactive",
} as const;

type Status = typeof Status[keyof typeof Status];
// type Status = "active" | "inactive"

function setStatus(s: Status) { /* ... */ }
setStatus(Status.Active); // works
setStatus("active");       // also works, same type

This pattern is fully erasable. The const object is plain JavaScript. The type Status line vanishes at strip time. The runtime behavior is identical, and in most cases type inference is equivalent or better, since string literal unions compose more cleanly with conditional types than numeric enum values do.

const enum exists as another option, but it comes with its own complications around isolated declarations and declaration files. The TypeScript team has long signaled that const enum has sharp edges, particularly in projects that share types across package boundaries. The const object pattern avoids all of them and plays well with every tool in the ecosystem.

Isolated Declarations and Parallel Builds

TypeScript 5.5 stabilized --isolatedDeclarations, a feature that allows tools to generate .d.ts declaration files from individual source files without needing full program type inference. The motivation is build performance: if a type-checker can process each file independently, declaration generation can be parallelized across all available cores rather than running as a single serial pass.

Code that satisfies --erasableSyntaxOnly tends to also satisfy the constraints of isolated declarations, since both require explicit type annotations in certain positions and discourage patterns that rely on implicit cross-file type inference. These two flags push in the same direction, and TypeScript 6.0 tightens the relationship between them.

For large monorepos with hundreds of packages, this combination matters. Bundler plugins and build orchestrators can generate declarations in parallel alongside compilation, rather than waiting for a sequential tsc pass. Projects running on Turborepo or Nx benefit directly from this since the per-package type-checking model maps cleanly onto their task graphs.

Module Resolution Catches Up to Reality

TypeScript’s module resolution story has been a consistent source of friction. The node10 resolution mode, formerly just node, has been the default despite being incompatible with how modern Node.js resolves ESM imports. TypeScript 4.7 added node16 and nodenext modes that match Node.js’s actual ESM resolution algorithm, including the requirement to use explicit file extensions in import paths.

TypeScript 5.0 introduced verbatimModuleSyntax, which prevents the compiler from rewriting or eliding import statements in ways that can cause runtime surprises with native ESM. TypeScript 6.0 continues moving the recommended defaults toward these modern module behaviors. The old commonjs and node targets are not removed, but the guidance and scaffolding pushes new projects toward nodenext.

This aligns with where the ecosystem has moved. Packages that ship both CJS and ESM are increasingly using the exports field in package.json to distinguish between them, a pattern that requires nodenext resolution to work correctly in TypeScript.

Breaking Changes and What Needs Attention

A major version bump means changes that could not ship in a 5.x release without opt-in flags. TypeScript 6.0 removes some long-deprecated configuration options and changes certain default behaviors, particularly around module and declaration emit. Before upgrading, it is worth running tsc --noEmit with the new version to surface any errors introduced by stricter defaults.

For most projects that have been keeping up with TypeScript 5.x releases, the migration is not disruptive. The groundwork for most 6.0 behaviors was laid in 5.5 through 5.8. The 6.0 release mostly flips defaults and removes escape hatches that were provided during the transition period.

The full list of breaking changes is covered in the official announcement.

What This Signals

TypeScript 6.0 is not a revolution in the way strict null checks were. It is a consolidation: the language formalizing a philosophy that has been building since native type stripping became viable. That philosophy is that TypeScript’s job is to check your types, not to rewrite your code.

The features that required transformation, enum and namespace especially, were added in an era when TypeScript was also trying to be an alternative module system and a metaprogramming layer. The JavaScript ecosystem has outgrown both needs. ESM handles modules. Stage-3 decorator proposals handle metaprogramming. TypeScript’s value now is almost entirely in its type system, and TypeScript 6.0 reflects that by making erasable TypeScript the recommended path for new projects.

For most codebases, the migration path is manageable. If you have been writing TypeScript with union types and const objects rather than enums, with explicit class fields rather than parameter properties, and with explicit type annotations in your public API surface, your codebase is likely already compatible with --erasableSyntaxOnly. The language is moving in a direction that rewards the patterns that were already considered idiomatic TypeScript.

Was this interesting?