TypeScript 6.0 landed on March 27, 2026, and the release is unusually coherent for a major version. Most of the changes connect to a single design principle: TypeScript should be annotations on top of JavaScript, not a separate language that compiles to JavaScript. Everything in this release either enforces that principle or is a consequence of it.
The erasable syntax line
The most important language-level addition is the --erasableSyntaxOnly flag, which debuted in TypeScript 5.8 but becomes a central compiler concern in 6.0. Enable it in your tsconfig and the compiler will reject any TypeScript syntax that requires code generation rather than simple token stripping.
{
"compilerOptions": {
"erasableSyntaxOnly": true
}
}
The three main offenders are regular enum declarations, namespace blocks with runtime bodies, and constructor parameter properties. All three require the compiler to synthesize JavaScript code that has no direct correspondence to the source text. A type eraser, like the one built into Node.js 22.6+ via Amaro, cannot handle them. It processes TypeScript the way a comment stripper processes code: it removes tokens, it does not transform logic.
This is the alignment TypeScript has been building toward since Node.js shipped --experimental-strip-types. With erasableSyntaxOnly enabled, you get a compiler-level guarantee that node --strip-types server.ts (now stable in Node.js 23, default in Node.js 24) will load your code without errors.
The flag also aligns TypeScript with the TC39 type annotations proposal, which covers only erasable constructs. If that proposal advances, erasableSyntaxOnly is effectively a JavaScript future-compatibility mode.
What to do with enums
The enum migration is the practical blocker for most codebases. Regular enums are not removed from the language, but they are incompatible with erasableSyntaxOnly and they fail at load time under Node.js’s native type stripping. The standard replacement is a const object paired with a derived union type:
// Before
enum Status {
Pending = 'pending',
Active = 'active',
Closed = 'closed',
}
// After
const Status = {
Pending: 'pending',
Active: 'active',
Closed: 'closed',
} as const;
type Status = typeof Status[keyof typeof Status];
// type Status = 'pending' | 'active' | 'closed'
The const object pattern is structurally compatible with everything the original enum was doing except nominal type identity. If you were relying on enum nominality to distinguish between two enums with the same underlying values, that is a harder migration. But for the vast majority of enums, the pattern above is a direct replacement.
const enum is unaffected. Values are inlined at compile time, producing no runtime artifact, so a stripper can erase the declaration without losing any behavior.
The Go compiler ships as a production option
TypeScript 6.0 also ships the Go-based compiler rewrite (codenamed Project Corsa) as a production-ready option. The JavaScript compiler is still present and still the default, but 6.0 is explicitly described as its last major release. TypeScript 7.x is where the Go compiler becomes the sole default.
The performance numbers are substantial. TypeScript’s own codebase goes from roughly 15 seconds to roughly 1.5 seconds for a full type-check. The VS Code repository drops from around 77 seconds to around 7.5 seconds. Peak memory falls 50 to 70 percent on large monorepos.
The architectural reason the rewrite needed to happen is worth understanding. TypeScript’s type checker is dominated by deeply recursive graph traversal with highly polymorphic call sites. V8 handles polymorphic call sites by degrading to slow megamorphic dispatch, which is exactly the wrong profile for type checking. The JavaScript type checker also runs on a single thread; Go’s goroutines let the compiler check independent files in parallel.
Go’s garbage collector was the other motivation over Rust. TypeScript’s type graphs contain cycles that Rust’s ownership model handles poorly without workarounds. Go’s GC handles them naturally, and the translation from the JavaScript codebase was more mechanical as a result.
If you write tooling on top of the TypeScript compiler API (ts-morph, typescript-eslint, custom transformers), test carefully before upgrading. The Go-native API is not yet a drop-in replacement for ts.createProgram. Microsoft is providing a JavaScript compatibility shim for the transition period, but the internal APIs that some tools relied on have changed.
Decorators and the NestJS problem
The experimentalDecorators and emitDecoratorMetadata flags are both removed in TypeScript 6.0. experimentalDecorators has been around since TypeScript 1.5 (2015) and implemented a pre-standard decorator draft that diverged significantly from the TC39 proposal that eventually reached stage 3. emitDecoratorMetadata was the more problematic companion: it injected Reflect.metadata calls encoding design:type, design:paramtypes, and design:returntype into emitted JavaScript, coupling TypeScript’s compile-time type information to runtime behavior.
For Angular Ivy (version 16+) and most first-party Angular code, the migration to TC39 standard decorators (available since TypeScript 5.0) is largely done. Angular moved away from emitDecoratorMetadata dependency before this removal.
The harder path is the NestJS and TypeORM ecosystem. TypeORM’s column type detection, class-validator’s validation metadata, and NestJS’s dependency injection container all relied heavily on design:paramtypes to discover constructor argument types at runtime. The TC39 decorator metadata proposal (implemented in TypeScript 5.2 via Symbol.metadata) provides a general mechanism, but it deliberately excluded automatic runtime type reflection. There is no replacement that gives you design:paramtypes without explicit annotation.
NestJS 10+ has migration paths for constructor injection that work with standard decorators. TypeORM’s path is more manual. If your codebase is deep in that ecosystem, budget time for this migration before upgrading.
--noCheck and the split build pipeline
A smaller but useful addition is the --noCheck flag. It emits JavaScript output without performing type checking, providing a cleaner alternative to --transpileOnly patterns that tools like ts-node offered.
# Type-check only, no output
tsc --noEmit
# Emit only, no type-check
tsc --noCheck
The appeal is that you stay within tsc for both operations, avoiding the edge cases where esbuild or swc output diverges slightly from TypeScript’s own emit. You can run type checking in a background process or CI step while the emit step stays fast.
--isolatedDeclarations reaches stable
Introduced as experimental in TypeScript 5.5, --isolatedDeclarations is now stable and first-class. It requires every exported declaration to carry enough explicit type information that a .d.ts file can be generated per-file, without cross-file inference.
// Not allowed under --isolatedDeclarations
export const multiply = (a: number, b: number) => a * b;
// Required
export const multiply = (a: number, b: number): number => a * b;
This matters architecturally because isolatedDeclarations is the prerequisite for the Go compiler’s parallelism to reach its full potential. When declaration generation depends on cross-file type inference, there are sequential dependencies between files. Remove that dependency and every file’s .d.ts can be generated independently, in parallel, by tools like Turborepo, Nx, or Rollup without waiting on the full type checker.
Library authors who enable both isolatedDeclarations and erasableSyntaxOnly are essentially opting into the TypeScript 7.x model early.
Module resolution cleanup
"moduleResolution": "node" is removed. This was the legacy CommonJS resolution algorithm predating Node.js 12, incompatible with the exports field in package.json, .mts/.cts extensions, and proper ESM semantics. The supported options are now node16, nodenext, and bundler.
Under node16 and nodenext, relative imports require explicit .js extensions even when the source file is .ts:
// Required under node16/nodenext
import { helper } from './utils.js'; // resolves to utils.ts at compile time
This looks odd but reflects how Node.js’s native ESM resolver actually operates. TypeScript resolves ./utils.js to ./utils.ts at compile time; the .js extension is correct at runtime. The bundler mode does not require explicit extensions, which is appropriate for applications going through Vite, webpack, or a similar bundler.
Where this leaves the ecosystem
The coherent story behind TypeScript 6.0 is that the language is converging on a model where types are annotations, not a compilation target. The Go compiler, erasable syntax enforcement, isolated declarations, and the removal of experimentalDecorators all point the same direction.
For most greenfield projects the upgrade is straightforward. The erasableSyntaxOnly flag is opt-in, the module resolution changes only break things if you were explicitly using "moduleResolution": "node", and the decorator removal only matters if you were on the experimentalDecorators path.
For established projects with heavy enum usage, deep NestJS or TypeORM integration, or custom compiler tooling, the migration requires real work. The TypeScript team’s timelines have been gradual enough that none of this should be a surprise, but 6.0 is the point where the language formally closes the door on the compile-everything era.