· 6 min read ·

TypeScript 6.0: Three Years of Borrowed Time, Paid Back

Source: typescript

TypeScript has maintained an unusual discipline throughout its life: minor versions add features, major versions clear the debt that minor versions cannot touch. From 1.0 through 5.x, the team held the major version counter at 5 for three full years, shipping features across thirteen minor releases without breaking anything. TypeScript 6.0, announced today, ends that sequence and settles the compatibility obligations the 5.x series had been carrying.

The more consequential story in this release is what the team finally removed.

The Long Tail of Legacy Module Resolution

Module resolution in TypeScript has caused confusion and genuine runtime bugs for years. The original --moduleResolution node mode, modeled after Node.js’s CommonJS resolution algorithm, predates the modern ESM/CJS split. It does not distinguish between the two module systems, does not understand exports maps in package.json, and handles .js extensions in ways that silently mismatch what runtimes do.

TypeScript 5.x introduced node16, nodenext, and bundler resolution modes that correctly model each environment. The team documented the old node mode as deprecated and flagged it in migration guides. Removing it required a major version, and TypeScript 6.0 makes that removal.

Projects sitting on "moduleResolution": "node" in their tsconfig.json will encounter errors on upgrade. The migration path depends on where the code runs:

// Before 6.0, worked through the entire 5.x series
{
  "compilerOptions": {
    "moduleResolution": "node"
  }
}

// For Node.js projects that care about the CJS/ESM boundary
{
  "compilerOptions": {
    "module": "nodenext",
    "moduleResolution": "nodenext"
  }
}

// For frontend projects using Vite, esbuild, webpack, etc.
{
  "compilerOptions": {
    "module": "esnext",
    "moduleResolution": "bundler"
  }
}

The migration has a real upfront cost, particularly for large codebases that never updated their tsconfig.json beyond what a scaffolding tool generated in 2019. The long-term benefit is that TypeScript’s resolution semantics finally match what runtimes and bundlers actually do. Errors that were silently swallowed under node resolution, particularly around exports conditions and type: "module" packages, become visible at type-check time rather than at runtime.

Erasable Syntax and the Native Execution Future

The --erasableSyntaxOnly flag shipped in TypeScript 5.8 and connects to a shift that has been building across the ecosystem. Node.js added experimental TypeScript support in version 22.6.0 through a strip-types mode; Deno has executed TypeScript natively since version 1.0; Bun runs .ts files directly. All of these approaches rely on type erasure: the runtime strips TypeScript type annotations without transforming any syntax, leaving behind valid JavaScript.

Type erasure works cleanly for type annotations, interfaces, and type assertions. It does not work for TypeScript-specific value-level syntax. Enums compile down to IIFE-wrapped objects. Namespace declarations compile to nested object assignments. Parameter properties in constructors expand to explicit property assignments in the constructor body. These constructs require a real transformation step, not erasure.

With --erasableSyntaxOnly enabled, TypeScript treats all of these as errors:

// All of these are rejected under --erasableSyntaxOnly

enum Direction {
  Up,
  Down,
  Left,
  Right
}

class Point {
  // parameter properties expand to constructor body statements
  constructor(public x: number, public y: number) {}
}

namespace Utils {
  // namespace used as a value, not just a type container
  export function identity<T>(x: T): T { return x; }
}

// Alternatives that survive erasure cleanly

const Direction = {
  Up: 0,
  Down: 1,
  Left: 2,
  Right: 3,
} as const;
type Direction = typeof Direction[keyof typeof Direction];

class Point {
  x: number;
  y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

Enums have been a friction point between TypeScript and the broader JavaScript ecosystem for years. They are a value-level construct with no JavaScript equivalent, which means every bundler, transpiler, and formatter has to handle them as a special case. Const enums have been a recurring source of bugs in isolatedModules setups, where the compiler cannot inline their values across file boundaries. The ecosystem had already been trending toward as const objects and union types; --erasableSyntaxOnly formalizes that direction.

The flag is not enabled by default in 6.0, but TypeScript 6.0 tightens the documentation around it and makes it a first-class recommendation for projects targeting native TypeScript execution. Teams that do not intend to run TypeScript directly in a runtime can continue using enums without issues.

Isolated Declarations and Parallel Build Speed

--isolatedDeclarations landed in TypeScript 5.5 and addressed a specific bottleneck in large monorepo setups. By default, TypeScript’s declaration emit requires full type inference across a file: determining the return type of an exported function may require following inference chains through multiple files. This serializes the declaration emit step.

Under --isolatedDeclarations, every exported symbol must carry an explicit type annotation. This constraint makes declaration file generation possible in a single pass, without cross-file resolution, which enables parallel execution across the package graph:

// Rejected under --isolatedDeclarations
// Compiler cannot emit a .d.ts without inferring the return type
export function parseConfig(input: string) {
  return JSON.parse(input) as Config;
}

export const defaultTimeout = 5000; // type inferred from literal

// Explicit annotations let each file be processed independently
export function parseConfig(input: string): Config {
  return JSON.parse(input) as Config;
}

export const defaultTimeout: number = 5000;

Tools like oxc, swc, and the Rollup DTS plugin can exploit this constraint to generate declaration files faster and in parallel. TypeScript’s own project references feature benefits as well. The ergonomic cost is the verbosity: explicit return types on every exported function runs against TypeScript’s usual inference-heavy style. For libraries and large internal packages where declaration emit is a meaningful portion of total build time, the trade-off is generally worth it.

TypeScript 6.0 does not make --isolatedDeclarations mandatory, but the guidance and tooling integration around it matures significantly.

What Gets Dropped

The removals in 6.0 go beyond module resolution. The --target ES3 and --target ES5 emit options are gone. These were meaningful in TypeScript’s early years; IE11 and legacy environments required them. For the majority of TypeScript users in 2026, they have not been practical requirements. Maintaining the emit logic for ES5 output, including the transformation of arrow functions, template literals, and class syntax, adds compiler complexity without serving current projects. Toolchains that genuinely need ES5 output, which are fewer each year, can route through Babel’s @babel/preset-env, which handles downleveling more flexibly than TypeScript’s emit was ever designed to.

The --out flag is removed. This option concatenated all TypeScript output into a single JavaScript file and predates TypeScript’s support for modules entirely. It was deprecated several major versions ago and incompatible with every modern module system.

The Node.js minimum version for running the TypeScript compiler itself moves up to Node.js 18. Node.js 16 reached end-of-life in September 2023. Maintaining compatibility with EOL runtimes is a maintenance cost with no corresponding benefit for projects on supported Node.js versions.

Ecosystem Impact

TypeScript’s compiler API has never carried a formal stability guarantee, yet nearly every TypeScript-adjacent tool depends on it: @typescript-eslint, ts-morph, the language server, language service plugins, and a large number of custom transformers. Major version transitions are historically when internal churn in the API surface shows up as breakage in downstream tooling.

TypeScript 6.0 introduces more explicit documentation around which parts of the compiler API are considered stable for external consumption and which are @internal. For tooling authors who build directly on the compiler API, reviewing the migration guide before upgrading is more important here than it was for any 5.x release.

For typical application code, the migration path is narrower. Updating moduleResolution in tsconfig.json, reviewing any enums that need to survive erasure, and running tsc --noEmit to surface what the new defaults expose covers most projects. The major framework integrations, including Vite, esbuild, and the various tsup/tsdown wrappers, will update to 6.0 compatibility quickly.

Reading the Version Bump

The TypeScript 5.x series was substantive: TC39 decorators, using and await using for explicit resource management, inferred type predicates, the NoInfer utility type, iterator helper types, and significant improvements to declaration emit performance. TypeScript 6.0 is the counterpart to that accumulation. It is a round of removal and alignment, clearing the API surface that three years of minor releases had been accumulating around.

ES3 emit, namespace-as-value syntax, and CJS-only module resolution were defensible choices when they were introduced. TypeScript was competing for adoption in a fragmented ecosystem, and backward compatibility bought it reach. That bet paid off. TypeScript 6.0 is the point where that debt gets settled, and the language comes out carrying less weight for it.

Was this interesting?