· 6 min read ·

TypeScript 6.0 and the Language It Had to Become

Source: typescript

TypeScript 5.0 shipped in March 2023. Between then and now, the team delivered a steady stream of minor releases, each adding features, improving inference, tightening the type system, and nudging module resolution toward sanity. That cadence trained everyone to expect incremental improvements on a predictable schedule, which makes the jump to 6.0 worth examining. Version numbers in TypeScript are not arbitrary; they signal a set of changes that could not ship without breaking backward compatibility in ways that require deliberate migration work.

Major version numbers in TypeScript have historically carried weight. TypeScript 2.0 introduced strict null checks, which fundamentally changed how the language reasoned about undefined and null. TypeScript 3.0 brought project references, enabling large monorepos to build incrementally. TypeScript 4.0 shipped variadic tuple types, which unlocked a class of generic patterns that had previously required arcane workarounds. TypeScript 5.0 added const type parameters and the standardized TC39 decorator syntax. Each jump to a new major version contained changes that required intentional migration work. TypeScript 6.0 follows that pattern.

The Erasable Syntax Problem

The most important trend building up to TypeScript 6.0 has been the pressure from native TypeScript execution. Node.js 22.6 added --experimental-strip-types support, allowing .ts files to run directly without a build step by removing type annotations at load time. Bun and Deno had been doing this for years. The fundamental constraint is that type stripping only works for TypeScript syntax that can be erased without affecting runtime behavior. A type annotation disappears cleanly; an enum does not.

TypeScript’s enum keyword compiles down to a JavaScript object and a self-executing function. That is not erasure, it is compilation. The same applies to namespace declarations, which produce actual JavaScript objects, and to parameter properties, which transform constructor(private name: string) into field declarations and assignments that require code generation. These three features became the friction point between TypeScript’s original feature set and the runtime world where Bun, Deno, and Node.js want to execute TypeScript files without a build step.

TypeScript 5.8 introduced --erasableSyntaxOnly as a compiler option. When enabled, it flags usage of enums, namespaces, and parameter properties as errors. The intent was explicit: give projects a migration path toward the constraints that native TypeScript execution requires. TypeScript 6.0 is where that path leads to a more permanent resolution.

Migrating Away from Enums

If you have relied on TypeScript enums, the migration path is well-established. The idiomatic replacement is a plain object with as const:

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

// After: const object, fully erasable
const Direction = {
  Up: "UP",
  Down: "DOWN",
  Left: "LEFT",
  Right: "RIGHT",
} as const;

type Direction = typeof Direction[keyof typeof Direction];

The as const version is more verbose, but it is plain JavaScript with a type layer on top. When you strip the types, what remains is a valid JavaScript object literal with no hidden ceremony. The type is derived from the value, not defined separately, which is the core principle of erasable TypeScript.

Namespaces have a more obvious replacement: ES modules. If you are using namespace for code organization, import and export across module files is the modern equivalent. The TypeScript docs have been nudging toward this since the 3.x era, and the case for namespaces in new code has been minimal for years.

Parameter properties require more care because they touch class design. The explicit version is not difficult to write, but it is more lines:

// Before: parameter property (non-erasable)
class User {
  constructor(private name: string, private age: number) {}
}

// After: explicit field declarations (erasable)
class User {
  private name: string;
  private age: number;

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

For small classes, this is a noticeable increase in boilerplate. For codebases that prioritize native execution compatibility, it is the price of erasing the TypeScript layer cleanly.

Isolated Declarations and Parallel Type-Checking

A second major thread in TypeScript’s recent development has been isolated declarations, introduced as an opt-in compiler option in TypeScript 5.5. The --isolatedDeclarations flag requires library authors to annotate exported declarations explicitly rather than relying on inference to generate .d.ts files.

// With --isolatedDeclarations, this is an error:
export function add(a: number, b: number) {
  return a + b; // return type must be explicit
}

// Explicit return type required:
export function add(a: number, b: number): number {
  return a + b;
}

The motivation is build performance. When TypeScript produces a declaration file for a module whose types are entirely inferred, it has to perform type-checking to do so, which creates a serial dependency chain between modules. With isolated declarations, each module’s output can be computed independently. This unblocks tools like esbuild and SWC from generating declaration files without running the full TypeScript compiler, which is a significant unlock for monorepos where incremental type-check times compound quickly.

The constraint is real. Requiring explicit return types on all exported functions is a style change that not every team will welcome. But for library authors publishing to npm, the downstream benefit to consumers using fast bundlers is meaningful. TypeScript 6.0 builds on this foundation, likely making isolated declarations a more central part of recommended library configurations.

Module Resolution, Finally Settled

TypeScript’s module resolution story has been a persistent source of confusion. The moduleResolution compiler option has progressed from node (legacy CommonJS behavior) through node16, nodenext, and bundler, each with different semantics around extension requirements and package.json exports field handling.

The underlying tension is that TypeScript historically allowed omitting file extensions in imports, as in import { foo } from './utils', because Node.js CommonJS supported this through its resolution algorithm. ES modules require explicit extensions: import { foo } from './utils.js' is what the spec demands. TypeScript grew multiple resolution modes to handle both worlds, which left many codebases on the legacy node resolution that produces subtle mismatches between what the type-checker accepts and what the runtime resolves.

A major version is the right place to stop carrying legacy defaults forward. TypeScript 6.0 moving toward making a modern resolution mode the default, or at minimum deprecating moduleResolution: node, reduces the category of errors that only appear at runtime after TypeScript has signed off on the code.

What This Means in Practice

The TypeScript ecosystem is large enough that breaking changes generate real migration work. Frameworks, test runners, and libraries with deep TypeScript integration need to validate against major releases. Teams that relied on enums throughout a large codebase face non-trivial refactoring. The TypeScript team has been telegraphing these specific changes through minor releases for long enough that most maintained projects have had time to prepare, but the work is still real.

The other side of that trade-off is that the changes being consolidated in 6.0 close a meaningful gap between TypeScript and the broader JavaScript tooling landscape. When Bun, Deno, and Node.js can all execute TypeScript natively through type stripping, and TypeScript 6.0 treats the erasable subset of the language as the preferred subset, the build step becomes optional for a significant class of use cases. That matters for developer experience in ways that compound over time: faster startup in development, simpler debugging, lower barrier to entry for developers new to the ecosystem.

The version number is not just accounting. TypeScript 6.0 represents a clear choice: closer alignment with the JavaScript runtime, not further divergence from it. The features that originally made TypeScript worth adopting, better type checking, richer editor tooling, gradual adoption from JavaScript, remain intact. What changes is the willingness to carry forward language constructs that require compilation rather than erasure. That is a trade-off the TypeScript team has been building toward for years, and a major version is the right place to make it permanent.

Was this interesting?