The Native TypeScript Compiler Cut Our Typecheck From 13s to 3.5s. The Only Hard Part Was ESLint.

We swapped in TypeScript 7's native Go compiler and our dashboard typecheck went from 13s to 3.5s. The compiler was nearly a drop-in. The real work was keeping typescript-eslint alive when native 7.0 ships no importable compiler API.
TypeScript 7, the Go port of the compiler, landed as a stable release on July 8. We tried it on our dashboard, and it was faster, so we moved. The whole migration was a four-PR stack, each part independently deployable.
That’s the boring version, and it’s mostly true. The compiler swap barely touched our code. The one part that took real thought was keeping typescript-eslint working, because native TypeScript 7.0 ships no importable compiler API.
What we were actually measuring
Our dashboard is a React app: 2,156 files, around 244k lines of TypeScript. We don’t use tsc to build anything. Vite and esbuild own the transpile. tsc runs with noEmit purely as a type-checker, which is exactly the workload the native compiler was built to speed up.
Two of those tsc runs sit on the pull-request critical path: the test:playwright:lint job, and the build prefix that runs before our Playwright end-to-end suite starts. A faster checker there shortens the merge queue and speeds up the typecheck we run locally on every change. That local win is the tsc command itself. Editor completion is a separate story I’ll come back to.
Here’s what we measured on a MacBook Pro M5, with a warm cache and an identical file set across both compilers:
| Task | tsc 5.9.3 | TS7 native | Speedup |
|---|---|---|---|
Full src type-check | 13.1s (~1.5 cores) | 3.46s (~4 cores) | ~3.8× |
test:playwright:lint (PR-critical) | 17.7s | ~4-5s (est.) | ~3.5-4× |
Two caveats before you get excited. This speeds up typechecking and nothing else: Vitest, the Playwright runtime, oxlint, and Vite bundling are all untouched, so if tsc is a small slice of your CI, the absolute win is small. And read the core counts in that table. The old compiler used about 1.5 cores, the native one about 4. Per core, the Go compiler is only around 1.4× faster here; the rest of the wall-clock win is parallelism. A 2-vCPU CI runner can’t spread across 4 cores, so measure on your own runner before you quote 3.8× to anyone. This pays off most when your tsc is a pure type-checker and typescript-eslint isn’t on your CI critical path.
The one real problem: no compiler API
The native typescript@7.0 package ships the tsc binary but no importable compiler API. That API returns in 7.1. Anything that does import 'typescript' to reach the compiler internals gets nothing back.
typescript-eslint does exactly that. Its type-aware rules load the compiler as a library. Point the typescript module at native 7.0 and ESLint crashes on startup.
This would have been a blocker if ESLint were load-bearing in our CI. It isn’t. Our CI linter is oxlint, and we run no ESLint pre-commit hook. typescript-eslint lives in one place only: the editor, for type-aware feedback while you code. So the requirement was narrow. Keep an API-bearing compiler for the editor, run the native binary everywhere else.
We could have dropped typescript-eslint’s type-aware rules and pointed typescript straight at native 7, which would have killed the split entirely. We didn’t, on purpose. Keeping the classic API avoids breaking editor integrations and keeps the rollout simple. And honestly, the editor matters less to us than it used to, now that most of the team works through coding agents. 7.1 restores the compiler API, so we’ll retire 6.0 completely then. If your team leans on editor type feedback and isn’t mostly working through coding agents, dropping the type-aware rules outright is the simpler call, and it skips the split entirely.
The packaging split
The fix is the one Microsoft documents for this exact transition. Install both compilers under different package names, and let the import path and the binary resolve to different things.
// package.json
{
"dependencies": {
// classic compiler: keeps `import 'typescript'` working for ESLint
"typescript": "npm:@typescript/typescript6@6.0.2",
// native Go compiler: provides the `tsc` binary for build + CI
"@typescript/native": "npm:typescript@7.0.2"
}
}
The name-and-binary split is the whole trick. import 'typescript' resolves to the classic 6.0 compiler, so typescript-eslint gets its API. The tsc binary comes from @typescript/native, so every script that invokes tsc, from the build to the Playwright lint job, runs native 7 with no script changes.
flowchart LR
PJ["package.json"]
PJ -->|"import 'typescript'"| C["@typescript/typescript6 6.0.2<br/>classic compiler + API"]
PJ -->|"tsc binary"| N["@typescript/native 7.0.2<br/>native Go compiler"]
C --> E["typescript-eslint<br/>editor type-aware lint"]
N --> B["build, cloudflare, playwright:lint<br/>CI + local typecheck"]
Pinning the classic side to @typescript/typescript6@6.0.2 isn’t a downgrade. That package bundles compiler 6.0.3, and we’d already stepped onto 6.0 in an earlier PR (more on that below), so nothing moved backwards. And it satisfies typescript-eslint’s typescript <6.1.0 peer requirement.
The split has a cost worth naming. Your editor’s type-aware lint now loads the classic 6.0 compiler, while CI runs the stricter native 7. A type that native 7 rejects can still show green in the editor until the merge queue catches it. The gap was tiny for us, the six errors below were the whole of it, but it’s a real seam, and if you lean hard on editor type errors you’ll want to know it’s there.
The rest of the migration was small
With ESLint handled, the code changes were minor. We did them in their own commit, on the old 5.9 compiler, so they carried no runtime risk and shipped independently of the compiler bump.
Two tsconfig settings had to go. Native 7 rejects baseUrl, so we dropped baseUrl: "./src" and re-rooted the paths aliases under ./src/ directly. No file relied on baseUrl-relative bare imports, so it was mechanical. It also forbids esModuleInterop: false. We already run moduleResolution: bundler, which implies the interop behavior anyway, so flipping it produced zero import errors.
The native compiler is stricter, and it flagged six type errors that 5.9 had let slide. Three were unparameterized generics, like a Filter[] that should have read Filter<JobMetric>[]. The other three were browser-polyfill globals in our Node polyfill shim, window.Buffer and its neighbors, which needed a real Window augmentation and a module declaration for process.
Six errors across a quarter-million lines. The type system is the same between 5.9 and 7; only the default strictness changed. Those stricter defaults just caught a few types we’d been sloppy about.
We also stopped at the TypeScript 6.0 transitional release on the way, as its own pull request, so any 6.0 deprecation defaults surfaced separately instead of tangling with the native swap.
What I’d tell you before you try it
If your tsc is typecheck-only, the native compiler is close to a drop-in, and the speedup is real. Spend your effort budget on one thing: finding what in your toolchain imports the compiler as a library. For us that was typescript-eslint, and the answer was to keep the classic compiler installed under the real typescript name and give the native binary its own.
One operational note. We didn’t pull the native packages the day they shipped. New npm packages sit for a week here before they reach our build, supply-chain hygiene that costs almost nothing. TypeScript 7.0 was fine to wait on. It wasn’t going anywhere.
If you run typescript-eslint in CI rather than only the editor, 7.0 is awkward, and waiting for 7.1 and its restored compiler API is the honest move. For everyone whose tsc is a checker and whose linter lives elsewhere, very little stands between you and a 3.8× faster typecheck.


