TypeScript has always been two things at once: a type checker and a compiler. For most of its history, that dual role created no real tension. You ran tsc, JavaScript came out, and the pipeline was simple. But the ecosystem grew up around TypeScript in ways the original design did not anticipate. Tools like esbuild and swc can strip and transpile TypeScript an order of magnitude faster than tsc, but only if they can process each file independently, without needing cross-file type inference to determine what to emit. Node.js 22 shipped --experimental-strip-types. Node.js 23 stabilized it. Deno has run .ts files natively for years.
TypeScript 6.0 is not primarily about new language features. It is about resolving the accumulated friction between TypeScript’s compiler role and the ecosystem that has grown up around it. The breaking changes and new defaults collectively push TypeScript toward a single responsibility: check your types. Let the other tools handle the rest.
Isolated Declarations Is No Longer Optional Infrastructure
The biggest architectural shift in 6.0 is the promotion of --isolatedDeclarations, which the team introduced as an opt-in flag in TypeScript 5.5. The flag requires that every exported declaration carry an explicit type annotation, sufficient that .d.ts output can be generated from a single file without consulting the rest of the codebase.
Without --isolatedDeclarations, TypeScript infers return types and property types across file boundaries. That inference is one of TypeScript’s most ergonomic features for application code, but it makes declaration generation a fundamentally serial operation. The type checker has to understand the whole graph before it can write any .d.ts files.
With explicit annotations enforced:
// Error under --isolatedDeclarations:
export function formatDate(d: Date) {
return d.toISOString().split('T')[0];
}
// Required:
export function formatDate(d: Date): string {
return d.toISOString().split('T')[0];
}
Now each file’s declarations can be emitted independently, in parallel, by any tool that understands TypeScript syntax, including tools that do not run the full type checker. This is what makes esbuild’s and swc’s declaration generation viable for large monorepos. Without explicit annotations, they have to fall back to tsc for .d.ts output anyway, which eliminates the build speed advantage.
--isolatedDeclarations is still opt-in in 6.0, but the surrounding infrastructure, including better error messages and tooling support, reflects the team’s view that this is the right default for library authors and increasingly for application code as well. The flag also has a useful secondary effect: it makes your public API surface explicit in the source, not just in the emitted declarations.
verbatimModuleSyntax Becomes the Default
This is the change that will produce the most immediate migration work. TypeScript 5.0 introduced --verbatimModuleSyntax, which enforces that type-only imports use import type syntax and that these are erased rather than preserved in emitted JavaScript. TypeScript 6.0 makes this behavior the default.
Prior to this change, TypeScript would silently elide imports that turned out to be type-only at emit time. This worked fine when tsc was your only transpiler, but it is catastrophic for single-file tools. When esbuild or swc encounter an import { User } from './models', they cannot know whether User is a value or a type without running the type checker. If they preserve the import and User is type-only, the emitted JavaScript has a runtime import that goes nowhere.
The fix is explicit:
// Before (worked in TypeScript 5.x without verbatimModuleSyntax):
import { User, createUser } from './models';
function greet(user: User): string {
return `Hello, ${user.name}`;
}
// After (required in TypeScript 6.0):
import type { User } from './models';
import { createUser } from './models';
The migration is largely mechanical. Running tsc --noEmit on your existing codebase after upgrading will surface every import that needs the type modifier. For most projects the changeset is repetitive but small. For codebases that have been sloppy about this, it surfaces real problems: places where the import was doing double duty as a type and a value, and the developer did not notice because tsc quietly handled it.
The Legacy Module System Gets Retired
TypeScript 5.x introduced --module nodenext, --module bundler, and better moduleResolution options to match how modern runtimes and bundlers actually resolve modules. TypeScript 6.0 completes this transition by removing or emitting hard errors on combinations that should have been retired years ago.
--moduleResolution node, the classic Node.js resolution from the CommonJS era, is deprecated with a migration path. --module commonjs without a corresponding modern target now produces warnings. The --target ES3 and --target ES5 options are removed entirely, along with the significant complexity they added to the emit pipeline: downleveling generators, async functions, optional chaining, and nullish coalescing for environments that have not existed in production for years.
For most teams, the migration is:
// tsconfig.json: before
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"target": "ES5"
}
}
// tsconfig.json: after
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "ES2022",
"verbatimModuleSyntax": true
}
}
For bundler environments, swap nodenext for bundler in both module and moduleResolution. The bundler resolution mode reflects how tools like Vite, webpack, and esbuild handle specifiers, including the absence of mandatory file extensions and the presence of exports field support in package.json.
Stricter Defaults and the noUncheckedIndexedAccess Conversation
TypeScript 6.0 also revisits what --strict enables. The most significant addition is --noUncheckedIndexedAccess, which makes array and object index access return T | undefined rather than T. This has been a requested inclusion for years, held back because it introduces noise in a large class of common patterns:
const items = ['a', 'b', 'c'];
const first = items[0]; // Previously: string. Now under noUncheckedIndexedAccess: string | undefined
if (first !== undefined) {
console.log(first.toUpperCase()); // Fine.
}
The objection to including this in --strict has always been legitimate: most code that indexes arrays is in contexts where the index is already bounds-checked by surrounding logic, and adding | undefined everywhere requires defensive code that feels redundant. The TypeScript team’s answer in 6.0 is essentially that the cost of enabling this is lower than the cost of missing the cases where it matters, especially as TypeScript is used more heavily in domains where index-out-of-bounds is a real failure mode.
Why This Version Number Matters
Major version bumps in TypeScript are rare. TypeScript 4.0 shipped in 2020, TypeScript 5.0 in 2023. Each major version has been an opportunity to remove deprecated features and make breaking changes that would be too disruptive in a minor release.
TypeScript 6.0’s breaking changes exist to clear the path for the ecosystem rather than to add capability. Removing ES3/ES5 targets simplifies the emit pipeline so future improvements are less expensive to implement. Making verbatimModuleSyntax the default removes an entire class of bugs that only appeared when using TypeScript with non-tsc tooling. Promoting isolated declarations enables build architectures that were theoretically possible in 5.5 but required manual enforcement.
The Node.js team did not wait for TypeScript to make these changes before shipping type stripping. Deno did not wait. The Bun runtime did not wait. What TypeScript 6.0 does is make the language’s own defaults consistent with how those runtimes expect TypeScript to be written. A .ts file that satisfies TypeScript 6.0’s defaults, with explicit type imports, explicit return types on exports, and modern module syntax, is a file that any runtime with type stripping can handle without a compilation step.
That was not a design goal when TypeScript was created. It has become one.
Migration Path
Upgrading to TypeScript 6.0 is a staged process. The first step is running the 6.0 compiler against your existing configuration with --noEmit and cataloging the errors before changing anything else. The three categories of errors you will encounter are type import violations (fixable with import type), missing explicit return types on exports (fixable with a codemod or LSP action), and deprecated module/target combinations (fixable by updating tsconfig).
For most applications, this is a half-day migration. For libraries with large public API surfaces and years of inferred return types, --isolatedDeclarations enforcement will take longer. The TypeScript team has published a migration guide, and the typescript-eslint rule @typescript-eslint/explicit-module-boundary-types produces the same annotations that --isolatedDeclarations requires, so teams that already use it are largely done.
The version number communicates that there is real upgrade work here. The changes underneath it point at where TypeScript is going: a type system you layer on top of JavaScript tooling you already have, rather than a pipeline that sits between your code and the runtime.