· 7 min read ·

TypeScript 6.0 and the Bet on Erasability

Source: typescript

TypeScript 6.0 is out, and the version number carries weight this time. This is not a 5.x increment with a fresh coat of paint. The changes are deliberate, some of them breaking, and they all point at the same underlying conviction: TypeScript should be designed to be erased, not compiled.

That sounds like a small distinction. It is not.

What “Erasable” Actually Means

Typescript has always generated JavaScript by stripping its own type annotations. But the old model required running tsc, which means loading your entire project, resolving all imports, running full type inference, and only then emitting output. Tools like esbuild, swc, and Babel figured out they could skip all of that and just strip the syntax directly, without understanding what any of the types meant. For most cases this worked fine. But there were edge cases where it broke: specifically, cases where a TypeScript construct carried runtime behavior that couldn’t be identified without type information.

The canonical problem was import elision. If you wrote:

import { Foo } from "./foo";

const x: Foo = getValue();

A type-aware compiler knows Foo is a type and elides the import. A dumb eraser sees a value import, leaves it in, and your bundle gains a side-effectful import that was never supposed to run. The importsNotUsedAsValues flag and later verbatimModuleSyntax were attempts to force this into safe territory. TypeScript 6.0 finishes the job by removing the old flags entirely and making verbatimModuleSyntax the only supported behavior.

With verbatimModuleSyntax required, every import that is type-only must be marked with import type. Every import that is kept must refer to at least one value. The rule is enforced at the syntax level, not the semantic level, which means any tool that parses TypeScript syntax can now emit correct JavaScript without understanding what types are.

// This is now required in TypeScript 6.0 under verbatimModuleSyntax
import type { User } from "./types";
import { createUser } from "./factory";

function registerUser(data: User): void {
  createUser(data);
}

The import type syntax has existed since TypeScript 3.8. In 6.0, it stops being a best practice and becomes the only correct way to import types.

isolatedDeclarations and Parallel Type Checking

The other major feature of this release, --isolatedDeclarations, was introduced as experimental in TypeScript 5.5 and graduates to a stable, recommended flag in 6.0. The idea is that every exported declaration in your code must have an explicit type annotation, enough information for a tool to generate its .d.ts file without looking at any other file in the project.

// This fails under --isolatedDeclarations
export function add(a: number, b: number) {
  return a + b; // Return type must be inferred from the body — not allowed
}

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

The practical payoff is significant for large projects. In a monorepo with a hundred packages, generating declaration files has traditionally required a serial pass through the dependency graph: package A’s types must be resolved before package B can be type-checked, because B imports from A. With --isolatedDeclarations, every package can emit its .d.ts files in parallel, since no file needs to look outside its own module to determine what it exports.

The TypeScript team has reported that this change can cut declaration emit time significantly in large monorepo builds. The exact numbers depend on your project structure, but the architectural unlock is real: you can now farm out .d.ts generation to multiple workers, the same way bundlers already parallelize transpilation.

The trade-off is annotation burden. You have to write explicit return types on every exported function, explicit types on every exported variable that would otherwise require inference across module boundaries. For most public APIs this is a good discipline anyway. For internal utility functions it is noise. The flag is opt-in, but expect it to become the norm in library code.

Module Resolution Defaults Finally Change

TypeScript 6.0 updates the default values for --module and --moduleResolution, which have been wrong for years. The old defaults were --module commonjs and --moduleResolution node, both of which reflect the Node.js ecosystem circa 2016. Most projects today either run through a bundler or target modern Node.js with ESM, and neither of those scenarios is well-served by the legacy defaults.

The new defaults depend on context, but for most configurations you should now be setting --moduleResolution bundler if you are going through webpack, Rollup, Vite, or any other bundler. For native Node.js ESM projects, --module nodenext with --moduleResolution nodenext is the correct pairing.

// tsconfig.json for a modern bundled project
{
  "compilerOptions": {
    "module": "preserve",
    "moduleResolution": "bundler",
    "verbatimModuleSyntax": true,
    "isolatedDeclarations": true
  }
}

The --module preserve option, introduced in TypeScript 5.4, tells the compiler to emit the module syntax exactly as written in the source without converting between ESM and CommonJS. Paired with --moduleResolution bundler, this is the right default for the era where your bundler handles the module format question and TypeScript just needs to get out of the way.

Legacy Flag Removal

Typescript 6.0 removes several flags that have been deprecated since 5.0:

  • importsNotUsedAsValues is gone, replaced by verbatimModuleSyntax
  • preserveValueImports is gone, same replacement
  • The --target ES3 and --target ES5 options are removed; the minimum target is now ES2015

The ES3/ES5 target removal is worth pausing on. TypeScript generating ES3 output has been a legacy compatibility path for code targeting Internet Explorer, old versions of Node.js, or embedded environments with very old JavaScript engines. Removing it sends a clear signal about where the language is focused. If you need ES5 output for some legacy reason, you will need to run your own transpilation step after TypeScript, or stay on TypeScript 5.x.

For most projects, none of this is a problem. But running tsc --noEmit after updating is the right first step before switching to emit, since some of these removals produce errors rather than warnings.

Node.js Native Type Stripping

This release lands at an interesting moment in the runtime ecosystem. Node.js 22.6 shipped experimental --strip-types support, which reached stability in Node.js 23. Deno has supported TypeScript natively for years. The TC39 Type Annotations proposal is progressing through committee with the goal of making type annotations a legal syntax that JavaScript engines ignore.

All of these developments point in the same direction TypeScript 6.0 is moving. The implicit promise of verbatimModuleSyntax plus isolatedDeclarations is that your TypeScript source can be processed correctly by a tool that understands syntax but not semantics. Node.js --strip-types is exactly such a tool. It removes type annotations using a fast parser pass, without running any type checking.

For a Discord bot or a small backend service, the development experience this enables is compelling. You write TypeScript, run it directly with Node.js 23 or later and --strip-types, skip the build step entirely during development, and only invoke tsc in CI to catch type errors. TypeScript 6.0’s design choices make this workflow reliable in a way it could not have been with the old flag semantics.

# Run TypeScript directly in Node.js 23 without a build step
node --strip-types src/index.ts

The constraint is that --strip-types will not do module format conversion, will not downlevel syntax, and will not handle any TypeScript feature that requires semantic analysis to remove. TypeScript 6.0 essentially formalizes the list of features that fall into that last category and makes them either illegal or clearly marked as requiring a full compile.

Migration Notes

For projects moving from TypeScript 5.x, the main steps are:

Replace importsNotUsedAsValues: "error" or "preserve" with verbatimModuleSyntax: true and audit all imports to ensure type-only imports use import type. The compiler will catch anything you miss.

Update moduleResolution from "node" to "bundler" if you are using a bundler, or to "nodenext" if you are targeting Node.js with ESM. The old "node" setting will produce a warning.

If you were targeting ES3 or ES5, you need a migration plan before upgrading. Either move your target to ES2015 or above, or add a separate transpilation step.

For library authors, enabling --isolatedDeclarations now will make your project forward-compatible with tooling that generates declaration files in parallel, and it enforces the kind of explicit API surface that consumers appreciate anyway.

The Bigger Picture

TypeScript’s relationship with its host ecosystem has evolved considerably since its early days as a compiler that generated ES3 and shipped its own module system. The language has spent the last several major versions cleaning up the technical debt from decisions made in 2012 to 2015, when the surrounding ecosystem looked very different.

Version 6.0 is the clearest statement yet that TypeScript sees itself as a layer on top of JavaScript, not a replacement for it. The compiler is still there, still the source of truth for type correctness, still necessary for generating declaration files and producing reliable output at scale. But the design choices in this release make TypeScript genuinely usable as a type annotation layer that any capable parser can strip, without the full weight of the type checker in the critical path.

For the tooling ecosystem, that shift unlocks real performance improvements and simpler integration stories. For day-to-day development, it mostly means fewer footguns around import handling and more predictable behavior between different tools in your chain. Neither of those things is glamorous, but they are the kind of improvements that compound over years of working in a codebase.

Was this interesting?