· 6 min read ·

TypeScript 6.0 and the Strippable Future It's Been Building Toward

Source: typescript

TypeScript 6.0 is officially out, and the headline feature is not a new type operator or a cleverer inference algorithm. The headline is a compiler flag called erasableSyntaxOnly, and understanding why it matters requires a short trip through TypeScript’s somewhat complicated history with its own syntax.

Two Kinds of TypeScript

Since the early days, TypeScript has contained two fundamentally different categories of syntax. The first category is pure type annotations: interface definitions, type aliases, generic parameters, : string on a function argument. These have no runtime representation. You can delete every token of that category and the resulting JavaScript runs identically. The second category is TypeScript-specific runtime constructs: enum, namespace, parameter properties (the constructor(private name: string) shorthand), and the older experimentalDecorators-style decorators. These require code generation. The compiler does not merely erase them; it rewrites them into JavaScript that behaves as intended.

For years, that distinction lived entirely inside the TypeScript compiler’s output logic. Tooling that wanted to “strip” TypeScript, like esbuild, swc, Babel’s TypeScript preset, and eventually Node.js itself, had to either implement the full transform or refuse to handle the constructs that required it. That tension has been building for a long time.

Node.js Made the Stakes Concrete

Node.js 22.6 shipped --experimental-strip-types, and Node.js 23 stabilized type stripping under --strip-types. The design of that feature is strict: Node will erase pure type annotations, but it will not transform enum declarations or run a namespace body. If your code contains those constructs, Node.js’s native TypeScript support simply fails on them.

This created a practical problem for developers who wanted to run TypeScript files directly in Node.js. The official TypeScript docs say you can use TypeScript; Node says you can run .ts files; but if you reach for enum out of habit or copy from an older codebase, you get a runtime error instead of a type error. That gap needed a compiler-side solution.

erasableSyntaxOnly is that solution. When you enable it, the TypeScript compiler reports an error if your source code contains any syntax that would require a transform rather than a strip. Specifically, it disallows:

  • enum declarations (except const enum, which inlines at compile time and leaves no runtime artifact)
  • namespace and module declarations with runtime bodies
  • Parameter properties in constructors
  • Legacy experimentalDecorators-style decorators
// Error under erasableSyntaxOnly: enum requires code generation
enum Direction {
  Up,
  Down,
  Left,
  Right
}

// Fine: const enum is inlined and fully erasable
const enum Status {
  Active,
  Inactive
}

// Error: parameter property requires constructor body rewrite
class User {
  constructor(private name: string) {}
}

// Fine: explicit property declaration
class User {
  private name: string;
  constructor(name: string) {
    this.name = name;
  }
}

If your code passes erasableSyntaxOnly, Node.js can run it natively. Deno can run it. Bun can run it. Any stripper-based tool can handle it without a special transform pass. The flag essentially certifies your code as compatible with the stripping-based ecosystem.

The —noCheck Flag

The second major addition is --noCheck. This flag tells the compiler to emit output without performing type checking. It sounds similar to --transpileOnly from ts-node, or to running your build with isolatedModules and skipping the type pass, but the implementation is different and the semantics are more coherent.

The key use case is split build pipelines. Many teams already run tsc --noEmit in CI for type checking and a faster tool like esbuild or swc for producing build artifacts. The problem is that neither approach uses the same compiler as the other, so you can hit edge cases where esbuild produces output that tsc would have handled differently, or where esbuild misses an import that tsc catches at emit time. --noCheck lets you use tsc itself for both phases, just separately:

# Type-check only, no output
tsc --noEmit

# Emit only, no type-check
tsc --noCheck

This also opens up better integration for watch mode and incremental builds, where you might want to see your changes reflected in output files immediately while the type checker catches up in the background.

Enums, Namespaces, and the Cost of Legacy Syntax

The erasableSyntaxOnly flag makes concrete what has been an unofficial community preference for years: don’t use enum or namespace in new TypeScript code. The reasons have always been there.

JavaScript enum interop is awkward. The compiled output for a Direction.Up becomes a number at runtime, which works until you try to serialize it, log it, or pass it across a module boundary that does not share the same enum definition. String literal union types (type Direction = 'up' | 'down' | 'left' | 'right') behave predictably, serialize naturally, and require zero runtime support.

Namespaces were TypeScript’s solution to module organization before ES modules were standardized. In a world where every project uses import/export, a namespace body adds indirection without benefit. The cases where namespaces remain genuinely useful, mostly for augmenting declaration files, are narrow enough that a lint rule discouraging general use has been common in style guides for years.

Parameter properties are more nuanced. They are genuinely convenient, and the transform they require is minimal. But they look nothing like standard JavaScript class syntax, which makes TypeScript code using them harder to read for developers who do not know the shorthand. The explicitness of separate property declaration and constructor assignment is more verbose but clearer.

With erasableSyntaxOnly, these preferences become enforceable at the compiler level rather than relying on linters or code review.

Module Resolution Defaults

TypeScript 6.0 also revisits module resolution defaults. The legacy "moduleResolution": "node" setting, which models CommonJS resolution from pre-Node.js 12, is increasingly disconnected from how modern Node.js and bundlers actually resolve modules. TypeScript 5.x introduced the bundler resolution mode and improved node16/nodenext. In 6.0, the pressure to move projects off the legacy default increases, with stronger messaging around the resolution modes that actually match current runtime behavior.

For new projects initialized with TypeScript 6.0 tooling, the defaults are better out of the box. For existing projects, the migration path is clearer than it has been in previous major versions.

What This Means for the Ecosystem

The isolatedDeclarations feature from TypeScript 5.5 required developers to annotate exported types explicitly enough that .d.ts files could be generated file-by-file. Combined with erasableSyntaxOnly, TypeScript 6.0 points toward a model where type information flows cleanly through the toolchain without requiring a full compile step in the critical path.

That matters most for large monorepos, where the cost of rebuilding all TypeScript before running tests or starting a dev server is measurable. If every file in your codebase is both erasable and has isolated declarations, a tool can process any individual file without understanding the full type graph. Build parallelism and caching become much more effective.

For library authors, erasableSyntaxOnly combined with isolatedDeclarations effectively defines a subset of TypeScript that is maximally portable across tooling. An npm package that passes both constraints can be used directly in environments that strip types natively, without requiring the consumer to configure a bundler or transpiler. That is a significant shift in how TypeScript libraries get distributed and consumed.

How to Adopt It

For a greenfield project or a project that already avoids enums and namespaces, enabling erasableSyntaxOnly is probably a one-line tsconfig change:

{
  "compilerOptions": {
    "erasableSyntaxOnly": true
  }
}

For an existing project with enums scattered through the codebase, the migration is more involved. String literal unions replace most enum use cases directly. const enum can stay if you need the inline behavior, since it passes the erasable check. Parameter properties need to be expanded by hand, though a codemod can automate most of it.

The --noCheck flag requires no migration. You can drop it into any existing build script where you currently rely on a third-party transpiler for speed.

The Bigger Picture

TypeScript started in 2012 with a bet that adding syntax to JavaScript, including syntax with runtime behavior, was the right way to build a type system for the web. That bet paid off in adoption. But the runtime-behavior constructs have always been the friction points: harder to interop with, harder to strip, harder to explain to someone coming from JavaScript.

TypeScript 6.0 does not remove those features, but it provides the tooling to formally declare that your codebase does not need them. That is a meaningful signal about where the language is heading, and it aligns TypeScript more closely with the direction the broader ecosystem has been moving for years. The future of TypeScript, based on everything in this release, is a language where types are genuinely just annotations, and the JavaScript underneath them is unchanged.

Was this interesting?