What Changed in TypeScript 6.0 (and Why 7.0 Is Written in Go)


I just spent a post going through What’s Actually Useful in TypeScript 5.x, the language features across the 5.x line that changed how I write code. I closed that one by pointing at the 7.0 beta, the native Go rewrite, and saying it was the thing to watch this year. This is the sequel to both halves of that.

Because between then and now, 6.0 shipped. It landed on March 23, 2026, and it’s a different kind of release than anything in the 5.x line. 6.0 is the last version built on the current JavaScript-based compiler, and Microsoft used it to clean house: modernize the defaults, remove a pile of legacy baggage, and get the ecosystem lined up for the Go-native 7.0 that follows it. So 6.0 is less “here are exciting new features” and more “here’s what’s going to break, and here’s why.”

That makes it worth a careful read before you bump the version in your package.json. This post is the breaking changes that actually matter, the handful of smaller additions worth knowing about, and then the part everyone is really asking about: what the Go rewrite means for your build.

Why this matters

If you’ve upgraded across 5.x releases, you’re used to a certain rhythm. A new minor version lands, you bump it, maybe you turn on a new flag, and your code keeps compiling. The occasional check gets stricter and catches a real bug, but wholesale breakage is rare.

6.0 is not that. It changes defaults that have been stable for years, and several of those changes will surface as errors in projects that compiled cleanly the day before. None of them are arbitrary. Each one removes a default that only made sense for a compiler that had to be backwards compatible with a much older ecosystem, and clearing them out is what lets 7.0 start from a cleaner baseline. But that doesn’t make the upgrade free.

So treat this as the migration guide I wish I’d had before I bumped it. The good news is that almost every break has a one-line fix, and most of them are pointing at something that was already a little wrong in your config.

The breaking defaults

These are the three that hit the most projects. If your build suddenly fails after the upgrade, it’s almost certainly one of these.

types now defaults to an empty array

This is the change most likely to surprise you, because it changes behavior you probably never configured in the first place.

Historically, if you didn’t specify a types array in your tsconfig.json, TypeScript would automatically include every @types/* package it could find in node_modules/@types. Convenient, until you realized it meant a transitive dependency could pull global types into your project that you never asked for. The classic version of this is @types/node showing up in a browser project because something three levels down depended on it, and suddenly process and Buffer are globals in your frontend code.

In 6.0, types defaults to an empty array. Nothing in node_modules/@types is included automatically anymore. You opt in to exactly the global type packages you want.

Before (TypeScript 5.x)
{
  "compilerOptions": {
    // no "types" field, so every @types/* package
    // in node_modules was pulled in automatically
  }
}
After (TypeScript 6.0)
{
  "compilerOptions": {
    "types": ["node"]
    // nothing global is included unless you list it
  }
}

The practical effect: if your code relied on ambient globals from a @types package you never explicitly listed, you’ll get a wall of “cannot find name” errors after upgrading. The fix is to add the packages you actually depend on to the types array. For most Node projects that’s ["node"], plus whatever test runner globals you use (["node", "vitest/globals"] and the like).

I see this as a strict improvement. The old behavior was implicit global state pulled from your dependency tree, which is exactly the kind of thing that’s invisible until it bites you. Making it explicit costs you one array, and in exchange you actually know what globals exist in your project.

rootDir now points at the tsconfig directory

The second one is subtler, and it changes where your output lands.

In older versions, when you didn’t set rootDir explicitly, TypeScript would compute it as the longest common path of all your input files. That sounds reasonable until you add or remove a file and the computed root shifts under you, which quietly changes your output directory structure. A file added in a new top-level folder could re-root the whole project and rearrange everything in outDir.

In 6.0, rootDir defaults to the directory containing the tsconfig.json instead of an inferred common path. It’s stable, predictable, and doesn’t move when your file layout changes.

Before (TypeScript 5.x)
{
  "compilerOptions": {
    "outDir": "./dist"
    // rootDir inferred from the common path of all inputs,
    // so dist/ structure depended on which files existed
  }
}
After (TypeScript 6.0)
{
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
    // set it explicitly so output layout is intentional
  }
}

If you never set rootDir and all your source happened to live under one folder, you might not notice a difference. Where it bites is projects with sources scattered at the repo root alongside the tsconfig, because now the root is the tsconfig directory and your dist/ layout shifts to match. The fix is the same fix that was always the right call: set rootDir explicitly to your source folder. Once it’s pinned, the change is a no-op.

Stricter side-effect imports

The third one tightens how the compiler treats imports you bring in purely for their side effects.

A side-effect import is the bare form with no bindings:

Syntax
import "./styles.css";
import "./register-globals.js";

You’re not importing a value, you’re importing the act of running the module. Previously the compiler was lenient about resolving these, and a side-effect import that pointed at a module with no real type information (or a path it couldn’t fully resolve) would often slide through quietly. The downside is that a typo in a bare import, or an import of something that doesn’t actually exist, could go unnoticed because nothing referenced a binding from it.

6.0 holds these to the same resolution standard as any other import. A side-effect import that can’t be resolved is now an error rather than a shrug.

Before (TypeScript 5.x)
import "./setpu-tests.js";
// typo'd path, but no binding was used, so it often
// slipped through without complaint
After (TypeScript 6.0)
import "./setpu-tests.js";
//     ~~~~~~~~~~~~~~~~~~~ Cannot find module './setpu-tests.js'.

This is the spiritual successor to noUncheckedSideEffectImports from 5.6, which I called out in the 5.x post as worth turning on. 6.0 essentially bakes more of that strictness into the defaults. If you’ve been running that flag already, you’ll see little change here. If you haven’t, expect a few of these to surface, and expect most of them to be real (a renamed file, a path that drifted, a polyfill import that no longer exists). That’s a good class of bug to have caught for you.

The smaller wins

Past the breaking changes, 6.0 does ship some genuine improvements. None of them are headline language features the way const type parameters or inferred type predicates were, but a couple are worth knowing about.

Reduced context sensitivity for this-less functions

This is an inference change that makes results more predictable.

When a callback doesn’t reference this, the compiler no longer treats it as context-sensitive in cases where it previously did. In plainer terms: functions that don’t care about this get their parameter types inferred more eagerly and more consistently, instead of getting caught up in the contextual-typing machinery that exists to handle this binding.

Example
declare function on<T>(event: string, handler: (payload: T) => void): void;
 
on("save", (payload) => {
  // in 6.0, payload's type resolves more predictably
  // because the handler never touches `this`
});

You won’t write code differently because of this. What you’ll notice, if anything, is fewer cases where inference produces a surprising type or where reordering arguments changes what gets inferred. It’s the kind of change that makes the compiler feel a little more boring, in the good way.

Subpath imports with #

6.0 improves support for Node’s subpath imports, the #-prefixed import specifiers you define in the imports field of package.json. These are the standards-based answer to the path-alias problem that everyone used to solve with tsconfig paths plus a bundler plugin.

package.json
{
  "imports": {
    "#utils/*": "./src/utils/*.js",
    "#config": "./src/config.js"
  }
}
Usage
import { formatDate } from "#utils/date.js";
import { config } from "#config";

The win here is that # imports are a runtime feature Node already understands, so unlike tsconfig paths, they don’t need a separate resolution step at build time to actually run. TypeScript 6.0 resolves and type-checks them properly, which means you can use one aliasing mechanism that both the compiler and the runtime agree on. If you’ve been maintaining parallel alias configs in tsconfig.json and your bundler, this is the path off that treadmill.

--stableTypeOrdering

A small flag with a specific audience: anyone who’s been bitten by non-deterministic output.

tsconfig.json
{
  "compilerOptions": {
    "stableTypeOrdering": true
  }
}

In some cases the order in which the compiler emitted members of union types or generated declaration output could vary between builds, depending on the order files were processed. For most people that’s invisible. For anyone diffing .d.ts output across builds, caching compiler output, or trying to get reproducible artifacts, it’s a real annoyance, because nothing actually changed but the diff is noisy. --stableTypeOrdering forces a consistent, deterministic ordering so the same inputs always produce byte-identical output.

If you cache build artifacts or ship declaration files and care about reproducibility, turn this on. If you don’t, you’ll never notice it.

Temporal types and getOrInsert

Two ecosystem catch-ups worth a mention. 6.0 ships type definitions for the Temporal API, the long-awaited replacement for Date, so as runtimes light up Temporal you’ll have proper types for it out of the box rather than reaching for a shim. And it adds typings for Map.prototype.getOrInsert and getOrInsertComputed, the “get this key or set and return a default” pattern that everyone has hand-rolled a hundred times.

Example
const counts = new Map<string, number>();
 
// instead of: if (!counts.has(k)) counts.set(k, 0)
const count = counts.getOrInsert("widgets", 0);

Neither changes how you architect anything, but both delete a small recurring bit of boilerplate, and that’s the kind of thing I’m always happy to see typed correctly.

What the Go rewrite actually means

Now the part that’s the real story of TypeScript in 2026.

7.0 is a native port of the TypeScript compiler and language service, rewritten in Go. Not a wrapper, not a partial rewrite, not a faster bundler step in front of the same compiler. The actual type checker, the thing that reads your code and decides whether it’s correct, reimplemented as a native binary. This is the project that shipped its first beta back in April, which I mentioned at the tail end of the 5.x post.

The reason it’s a big deal is performance, and the reason it can be that much faster comes down to two things the current compiler can’t do. First, it compiles to native code instead of running as JavaScript on a JS engine, so it skips the overhead that comes with that. Second, and this is the larger one, Go gives it real shared-memory multithreading. The current tsc is effectively single-threaded for the core checking work, because the JavaScript model it’s built on doesn’t share memory across threads the way the type checker would need. A native implementation can spread the checking work across cores against shared state, which is exactly the shape of work that benefits from it.

Put together, Microsoft is targeting roughly a 10x improvement, and the early numbers have held up across real codebases, not just toy benchmarks. The figure they keep pointing at is a large project that takes well over a minute to check under today’s tsc dropping to under ten seconds under the native build.

Here’s what I want to be clear about, because it’s easy to over-read a “10x faster” headline:

The language doesn’t change. It’s the same type system, the same inference, the same checks. Everything in my 5.x post and everything in the 6.0 section above behaves identically. 7.0 is a faster implementation of the exact same language, not a new language. Your code that type-checks today type-checks under the native compiler, with the same errors in the same places.

The 10x is for the slow stuff. If your project checks in two seconds today, you will not be blown away, because two seconds wasn’t your problem. Where this lands hard is the large monorepo, the project where tsc --noEmit in CI takes minutes, the editor that lags on a big codebase because the language service is grinding. That’s the workload the native rewrite is built for, and that’s where the difference is the kind you feel.

The editor experience is part of it. It’s not just batch builds. The language service is what powers autocomplete, go-to-definition, and inline errors in your editor, and it’s being ported too. The payoff there is responsiveness on large projects, the cases where today you type and wait for the squiggles to catch up. A native language service is the fix for that lag.

This is also exactly why 6.0 looks the way it does. All that clearing out of legacy defaults, the implicit @types inclusion, the inferred rootDir, the lenient import resolution, is the kind of accumulated behavior that’s much harder to faithfully reimplement in a brand new codebase. 6.0 trims it on the JavaScript side first so the Go implementation starts from a cleaner, more predictable baseline. The breakage in 6.0 is the price of the speed in 7.0.

What I’d do about it

Concretely, here’s how I’m treating this sequence.

Upgrade to 6.0 deliberately, not casually. Read the three breaking defaults above before you bump it, and budget a little time for the errors they’ll surface. Most are one-line config fixes, and most are pointing at something that was already slightly off (an implicit global, an unpinned rootDir, a stale import). Set types explicitly, pin rootDir, and let the stricter side-effect imports flush out any dead paths.

While you’re in the config, the additions worth turning on are situational: stableTypeOrdering if you care about reproducible output, and the move to # subpath imports if you’re tired of maintaining alias config in two places.

Then watch 7.0. If your builds or your editor feel slow on a large project, the native compiler is the single biggest lever you’ll get this year, and it costs you nothing in terms of how you write code, because it’s the same language underneath. If your projects are small and fast, you can let it stabilize and pick it up whenever, you’re not missing language features by waiting.

The throughline from the 5.x post still holds. The 5.x line was about features that changed how I write TypeScript. 6.0 and 7.0 aren’t about that at all. They’re about the compiler itself growing up: shedding the defaults that no longer made sense, and then getting fast enough that on a big codebase you stop noticing it’s running. After years of “TypeScript is great but the build is slow,” that second part is the one I’ve been waiting for.

If you want the long-form details, the TypeScript release notes cover every change in 6.0, and the native compiler work is worth following directly as the betas roll toward a stable 7.0. This post is the highlight reel and the migration heads-up. The release notes are the source.