TypeScript 6.0 is now available, and the headline features matter less than the architectural direction they confirm. This release is the formal conclusion of a transition that has been building since TypeScript 5.0: the language is stepping back from being a compile-to-JavaScript superset and committing to being a type annotation layer.
That distinction sounds subtle, but it has real consequences for how you write TypeScript, which tools you need, and what “building” a TypeScript project even means.
The Two TypeScripts
For most of TypeScript’s history, the language occupied an uncomfortable dual role. On one hand, it was a type system grafted onto JavaScript: interfaces, type aliases, generics, conditional types. These features have no runtime representation; the TypeScript compiler strips them away, and the emitted JavaScript is semantically identical to untyped code.
On the other hand, TypeScript shipped its own runtime-emitting constructs: enums, namespaces, parameter properties in constructors, and the old experimentalDecorators mode. These features produce real JavaScript output. An enum is not erased; it becomes an immediately invoked function that mutates an object. A namespace with values becomes an IIFE. A parameter property becomes a constructor body assignment.
// This TypeScript enum...
enum Direction { Up, Down, Left, Right }
// ...becomes this JavaScript:
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 = {}));
This dual role created real friction for the tooling ecosystem. Build tools that wanted to strip TypeScript types quickly could not blindly erase type syntax; they had to either transform enums and namespaces (doing real compiler work) or refuse to handle them. Node.js’s native TypeScript support, which arrived in v22.6 via --experimental-strip-types, took the latter approach: it handles only erasable syntax. Enums, namespaces with values, parameter properties — all blocked.
erasableSyntaxOnly Makes the Model Explicit
TypeScript 5.8 introduced the erasableSyntaxOnly compiler flag, which errors on any TypeScript syntax that requires code emission rather than type erasure. Enabling it turns the conceptual divide between “TypeScript that erases” and “TypeScript that compiles” into a hard compiler enforcement.
{
"compilerOptions": {
"erasableSyntaxOnly": true
}
}
// With erasableSyntaxOnly: true, these are compile errors:
enum Status { Active, Inactive } // Error: not erasable syntax
namespace Config { export const x = 1; } // Error: not erasable syntax
class Foo { constructor(private x: number) {} } // Error: parameter property
// These are fine — pure type annotations, fully erasable:
interface User { name: string; age: number; }
type ID = string | number;
function greet(name: string): void { console.log(name); }
TypeScript 6.0 elevates this flag from an opt-in discipline to the recommended default for new projects. With it enabled, your codebase becomes compatible with Node.js type stripping, Deno, Bun, and any build tool that handles TypeScript by stripping annotations rather than running a full compilation pipeline.
The practical payoff for local development and simple deployments:
{
"scripts": {
"start": "node --strip-types src/index.ts"
}
}
No tsc, no ts-node, no tsx. Your TypeScript files run directly in Node.js because the types are annotations on otherwise valid JavaScript, and stripping them requires no semantic understanding of the program.
isolatedDeclarations Unlocks Parallel Declaration Emit
The other foundational change is isolatedDeclarations, introduced in TypeScript 5.5. It is a constraint on how you write exported APIs: every exported symbol must have an explicit type annotation, such that any single file’s .d.ts declaration can be generated without type-checking the rest of the program.
// Without isolatedDeclarations, this is fine:
export function computeTotal(items: number[]) {
return items.reduce((a, b) => a + b, 0);
// return type inferred by checking the body
}
// With isolatedDeclarations: true, the return type must be explicit:
export function computeTotal(items: number[]): number {
return items.reduce((a, b) => a + b, 0);
}
The annotation requirement sounds like overhead until you understand what it unlocks. Build tools can generate .d.ts declaration files for every file in a monorepo in parallel, without any inter-file type resolution. oxc, esbuild, and swc can call TypeScript’s transpileDeclaration API on each file independently and aggregate the results. For large TypeScript monorepos, this moves the bottleneck from a single sequential type-check pass to parallelizable per-file work, reducing declaration emit from minutes to seconds at sufficient scale.
The tradeoff is real: you write more annotations. For exported functions in a public API, explicit return types were already recommended practice. isolatedDeclarations enforces what maintainable library code does voluntarily, and TypeScript 6.0 makes that expectation official for anyone building for distribution.
Breaking Changes and Long-Overdue Cleanup
TypeScript 6.0 removes several features and modes the language has been carrying as backward-compatibility weight.
moduleResolution: node (also node10), the legacy default that predated proper ESM support and package.json exports fields, is gone. Modern projects should use node16, nodenext, or bundler. These modes correctly handle conditional exports, .js extension requirements for ESM, and the exports field that most packages now rely on. The old mode silently produced incorrect resolution behavior for anything that used subpath exports.
The importsNotUsedAsValues and preserveValueImports options are removed, having been superseded by verbatimModuleSyntax since TypeScript 5.0. That flag enforces the right model: type-only imports must be explicitly marked with import type, so that tree-shaking and type-stripping tools can reliably distinguish them from value imports.
// verbatimModuleSyntax enforces this separation:
import type { User } from "./types"; // erased at build time
import { createUser } from "./factory"; // kept in output
// This is now an error if User is only used as a type:
import { User } from "./types"; // Error: use 'import type'
Support for target: ES3 is removed, ending a backward-compatibility story that required special-casing property access syntax and getter/setter emit for environments that no longer exist in any practical deployment context.
Migration
For codebases following modern TypeScript conventions, the upgrade path is straightforward. The two changes most likely to require code modifications are enum usage and module resolution settings.
Enums can be replaced with const objects paired with a derived union type, which has been a common TypeScript idiom for years:
// Replace:
enum Direction { Up = "UP", Down = "DOWN" }
// With:
const Direction = { Up: "UP", Down: "DOWN" } as const;
type Direction = typeof Direction[keyof typeof Direction];
This pattern is fully erasable, tree-shakeable, and semantically equivalent for most enum use cases. The one category that requires more thought is numeric enums used for bitmask operations, where the auto-incrementing integer assignment is part of the design; those need explicit numeric literals in the const object.
Module resolution migration is largely a matter of updating tsconfig.json and ensuring relative imports in ESM contexts use .js extensions where required by node16 or nodenext mode.
What Drove This
TypeScript was designed at a time when compile-to-JS languages were a reasonable bet. CoffeeScript, Dart, and others were real competitors, and the ability to extend JavaScript with runtime constructs seemed like a way to provide additional value beyond type checking. JavaScript has since absorbed most of what those languages offered, and the tooling ecosystem has standardized on type erasure as the minimal, composable way to integrate TypeScript into a build pipeline.
The stage 3 decorator spec, which TypeScript 5.0 adopted in place of the old experimentalDecorators approach, is itself a sign of this: TypeScript is no longer the place where JavaScript language proposals get prototyped. The language is waiting for TC39 now, not running ahead of it.
TypeScript 6.0 is the release where the language’s own design aligns with that reality. The type system is the reason to use TypeScript, and everything around it should be as transparent and removable as possible.