TypeScript 7 and Oxc: migration notes
The JavaScript and TypeScript toolchain is going through a fairly significant change. TypeScript's compiler is moving to a native Go implementation, while projects such as Oxc are rewriting linting and formatting tools in Rust.
I have used both in a few projects. The speed difference is real, but the benchmark numbers are not the most interesting part. Once type checking, linting, and formatting become fast enough, they stop feeling like separate steps you have to wait for. That changes how you work.
At the same time, the tools are not all at the same stage. Oxlint is relatively easy to introduce alongside ESLint. Moving to TypeScript 7 needs more care because a lot of tooling depends on the TypeScript compiler API. Oxfmt is still beta, so I would treat it differently again. This is what I found during the transition and how I would approach it today.
TypeScript 7
TypeScript 7 was released on July 8, 2026. The biggest change is not the TypeScript language itself. It is the compiler implementation. TypeScript 7 ships with a native Go-based compiler, and the goal is not to redesign the language or change how TypeScript programs behave. The goal is to keep the existing compiler's behavior while making it substantially faster.
Microsoft's published benchmarks show very large improvements on large TypeScript projects. In the VS Code repository, for example, full type checking drops from roughly 125 seconds to around 10 seconds in one of the published measurements. You should not take that number and assume your project will get the same result. Real projects have disk I/O, process startup, cache effects, different CPU configurations, project references, generated files, and monorepo-specific overhead. The general result is still clear: large TypeScript projects can get a major compiler speedup.
Why Go?
The obvious question is why Go rather than Rust. The TypeScript compiler works with large, interconnected data structures and graphs. Rust's ownership model makes a direct port of that architecture difficult, and a Rust rewrite would likely have required much more of the compiler to be redesigned rather than simply moved to a native implementation. Go is lower-level than JavaScript while still having a garbage collector. It produces native binaries, works well with cyclic data structures, and allowed the TypeScript team to preserve much more of the existing compiler architecture.
That is an important distinction. The reason is not simply that Go is "faster than Rust." The goal was to move the existing compiler to native code without spending years redesigning it, and Go fit that requirement better.
So the TypeScript 7 performance improvement is not just a consequence of using Go. Native execution, changes to the compiler architecture, and parallel work all contribute to the result.
Parallel type checking
One of the important changes in TypeScript 7 is parallelism. Type checking can use multiple checkers, and project-reference builds can also be performed in parallel:
tsc --checkers 4 --builders 4
tsc --singleThreaded
More workers do not automatically mean a faster build. CPU usage matters, but memory usage matters too. On CI machines, increasing the number of checkers can push memory consumption high enough to become the new bottleneck. I would not simply set this to eight workers because eight sounds better. Measure it on the machine that actually runs your builds.
The editor experience
The compiler speedup is probably going to be most noticeable during development. On a large TypeScript project, saving a file and waiting for type errors to update was a real interruption. With the native compiler and parallel work, that delay can become much smaller.
There is an important distinction here, though: the compiler and the language server are not the same thing. A faster tsc does not automatically mean every editor operation becomes faster by exactly the same amount. VS Code and other editor integrations depend on their own versions and on the TypeScript APIs they use. That is why upgrading to TypeScript 7 should not stop at tsc --noEmit. Check the editor, the build system, and anything else in the project that talks directly to TypeScript.
Trying it in an existing project
The first step is simple:
npm install -D typescript@7
npx tsc --version
npx tsc --noEmit
Starting with --noEmit makes sense. You can test the new compiler without changing your build output.
For an older project, I would also avoid jumping straight from TypeScript 5 to TypeScript 7. TypeScript 6 removed or deprecated a number of older behaviors and options, and TypeScript 7 removes APIs that were deprecated earlier. Going through TypeScript 6 first makes it much easier to tell which problems are old deprecations and which ones are actually caused by the TypeScript 7 migration.
The difficult part: the compiler API
For me, the compiler API is more important than the raw speed improvement. A lot of TypeScript tooling does not just execute tsc. It imports TypeScript directly:
import * as ts from "typescript"
These tools may build ASTs, transform ASTs, inspect types, generate code, or depend on compiler internals. That includes tools such as:
- typescript-eslint
- ts-morph
- custom transformers
- code-generation tools
- Vue, Svelte, Astro, Angular and MDX tooling
- internal TypeScript compiler tools
This means the TypeScript version is also a dependency of your toolchain. With TypeScript 7, the compiler API story is different from previous releases. The new API is still being developed, so you cannot assume that every tool which worked with TypeScript 6 will immediately work with TypeScript 7.
The right approach is to check each dependency before upgrading. If a tool does not support TypeScript 7 yet, there is no reason to force the entire project through a broken migration. Depending on the tool, you may be able to keep the older TypeScript API available temporarily or wait for the tool to catch up. The important thing is not to add two TypeScript versions to a project blindly. Follow the compatibility instructions of the specific tool.
Oxc
Oxc is a collection of JavaScript and TypeScript tooling written in Rust. It is not a single tool but a set of pieces stacked on each other:
flowchart TD
parser["parser"] --> linter["oxlint"]
parser --> formatter["oxfmt"]
parser --> transformer["transformer"]
parser --> minifier["minifier"]
resolver["resolver"] --> linter
resolver --> transformer
linter -.->|type info| tsgolint["tsgolint"]
The parser turns source into an AST, the resolver handles module resolution, the transformer does the TypeScript and JSX work, and the minifier shrinks production output. The linter and formatter sit on that same AST, so a file is not parsed again for every tool. When a rule needs type information, oxlint reaches for tsgolint, which I get to shortly. Rolldown builds on this foundation as well. Two pieces matter for this article: oxlint on the linting side and oxfmt on the formatting side.
They are not equally mature. Oxlint is the one I would be comfortable introducing into an existing project today. Oxfmt is more interesting as an experiment because it is still beta. I would not use the same migration strategy for both.
Moving from ESLint to Oxlint
Oxlint's biggest advantage is speed. Installation is straightforward:
pnpm add -D oxlint
pnpm oxlint
pnpm oxlint --fix
There is also a migration tool for converting an existing ESLint configuration:
npx @oxlint/migrate
I would still keep ESLint around initially and run both:
{
"scripts": {
"lint": "oxlint && eslint ."
}
}
Let them run together for a while. This gives you time to compare the results and see which ESLint rules are already covered by Oxlint. Once the overlap is understood, eslint-plugin-oxlint can disable rules in ESLint that Oxlint is already checking. The setup becomes roughly:
flowchart LR
src([source]) --> oxlint["oxlint"]
src --> eslint["ESLint"]
oxlint --> out([lint result])
eslint --> out
Oxlint races through the rules it covers, and ESLint only handles what Oxlint has no equivalent for.
That is much safer than deleting ESLint on day one. If the project has custom ESLint plugins or internal rules, you can keep them. As Oxlint's rule coverage improves, the amount of ESLint you need can gradually shrink.
Oxlint configuration
A simple configuration can look like this:
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"categories": {
"correctness": "error",
"suspicious": "warn",
"pedantic": "off"
},
"rules": {
"no-console": "warn",
"typescript/no-explicit-any": "error"
},
"ignorePatterns": [
"dist",
"build",
"**/*.generated.ts"
],
"overrides": [
{
"files": [
"**/*.test.ts",
"**/*.spec.ts"
],
"rules": {
"no-console": "off",
"typescript/no-non-null-assertion": "off"
}
}
]
}
You do not need to configure every rule individually. I prefer starting with categories and then overriding the handful of rules where the project needs different behavior. The migration tool is useful, but review the generated configuration. ESLint plugins do not always have a one-to-one equivalent in Oxlint, and a rule that disappears during migration should be a conscious decision rather than an accident.
Type-aware linting
One of Oxlint's early limitations was type-aware linting. Consider this:
async function saveUser(user: User): Promise<void> {
await db.users.update(user)
}
async function handler(req: Request) {
saveUser(req.user)
return new Response("ok")
}
The problem here is not syntax. saveUser() returns a Promise and the caller does not wait for it. To detect that reliably, the linter needs to know the return type of saveUser(). That is where type-aware linting comes in.
Oxc uses TypeScript's compiler infrastructure for this part through tsgolint. The architecture is roughly:
flowchart LR
subgraph rust["Rust side"]
oxlint["oxlint"] --> rules["normal lint rules"]
end
subgraph go["Go side"]
tsgolint["tsgolint"] --> ts7["TypeScript 7<br/>program and type information"]
end
oxlint -->|rule needing types| tsgolint
So it would be inaccurate to say that Oxlint reimplemented the TypeScript type system in Rust. The Rust side handles the linting work, and when a rule needs type information, the TypeScript compiler infrastructure provides it.
The exact installation and configuration details depend on the Oxc version you are using, but the basic idea is:
pnpm oxlint --type-aware
Type-aware linting and TypeScript compiler diagnostics are related but not identical. It is useful to keep the distinction clear:
typeAwareenables lint rules that need type information.typeCheckruns TypeScript's own type-checking diagnostics as part of the analysis.
Why type-aware rules matter
Consider:
function greet(name: string) {
if (name !== undefined) {
// ...
}
}
name is already a string, so the condition is unnecessary. Another common example:
const limit = input.limit || 20
If zero is a valid value, this can produce the wrong result. Writing input.limit ?? 20 instead only falls back for null or undefined.
These are not problems that a linter can reliably solve from syntax alone. It needs type information, which is why type-aware linting is more expensive than normal AST-based linting.
How to read the performance numbers
Oxc's published benchmarks show substantial speedups for type-aware linting compared with ESLint and typescript-eslint:
| Repository | ESLint + typescript-eslint | tsgolint |
|---|---|---|
| microsoft/vscode | 83.2 s | 6.96 s |
| microsoft/typescript | 27.2 s | 1.94 s |
| typeorm/typeorm | 13.2 s | 0.75 s |
| vuejs/core | 12.3 s | 0.95 s |
These are benchmark results, not promises about your CI. There are several things worth separating. Normal Oxlint and type-aware Oxlint are not the same workload. The TypeScript program also has to be created, configuration has to be resolved, and source files have to be read, and on a small project those fixed costs can make the relative difference much smaller. So I would not tell a team that "Oxlint is 18 times faster" and expect that number to show up in production. Measure your own CI.
Type-aware linting and tsconfig
Once type-aware linting is enabled, the TypeScript project structure matters more. If a monorepo has unnecessarily broad include patterns, linting can become more expensive than it needs to be. Generated files, build output, and test fixtures should not accidentally become part of the TypeScript program.
Project references matter as well. If one package depends on generated .d.ts files from another package and those files do not exist yet, type-aware tooling may not have the type information you expect. Before enabling type-aware linting across a large monorepo, make sure the existing TypeScript project structure is already clean.
Oxfmt
Oxfmt is Oxc's Rust-based formatter and it is still beta. The goal is to provide a Prettier-compatible formatter while being substantially faster. Oxc's own benchmarks show large performance differences compared with Prettier and Biome. Again, those numbers should be treated as benchmark results rather than a guarantee that every project will see the same improvement.
The interesting part of Oxfmt is not just speed. It supports JavaScript and TypeScript as well as formats such as JSON, YAML, TOML, HTML, Vue, CSS, Markdown, and GraphQL, and it can also handle things such as import sorting and Tailwind class sorting.
But this is where I am more conservative than I am with Oxlint. A formatter touches the whole file. A bad lint rule may report a few lines, while a formatter can rewrite thousands of files and produce a huge diff. That makes formatter migrations much more disruptive. I would not run a beta formatter across the entire repository in one giant commit. Use a separate branch, inspect the diff, compare the output with the existing formatter, and check how it behaves with the rest of the toolchain.
Should you leave Prettier immediately?
No. Oxfmt is interesting enough to try today, and the performance difference is significant. But formatter stability matters more than formatter speed.
In a large repository, changing formatters is also a Git-history problem. If every file gets rewritten, real code changes become harder to review and git blame becomes less useful. My order would be:
- oxlint: easy to try today.
- TypeScript 7: migrate after checking the toolchain.
- oxfmt: worth experimenting with while it is beta.
CI and pre-commit
CI is where the speed improvements become particularly useful:
jobs:
static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm oxlint --type-aware
- run: pnpm tsc --noEmit
- run: pnpm oxfmt --check
I prefer keeping linting, type checking, and formatting as separate CI steps. It is not just about performance: when CI fails, you immediately know which check failed. On a larger repository, these can become separate jobs and run in parallel. On a small project, I would not add a complicated CI setup just to save a few seconds.
I am more selective about what runs in a Git hook:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"oxlint --fix",
"prettier --write"
],
"*.{json,css,md,yaml}": "prettier --write"
}
}
I would not put type-aware linting in the hook. Even if only one file changed, the TypeScript program may still need to be built, and on a large project that is too much work for every commit. Fast, file-oriented checks belong in the hook, and the heavier analysis can stay in CI.
If Oxfmt becomes the project's formatter later, that prettier --write line becomes oxfmt. But I would make that change after deciding that Oxfmt is stable enough for the repository.
What I measured myself
I made these changes in a few projects. I did not see the 10x or 20x improvements from official benchmarks in every project, which is expected. A real project is not just the linter. There is file discovery, configuration loading, process startup, Git hooks, package-manager overhead, and sometimes other processes competing for the same machine. Some of those costs do not change when you replace the tool, so a benchmark might show a 15x improvement while a real project sees 4x or 5x.
That is still a big difference. If a pre-commit check goes from 20 seconds to 4 seconds, people behave differently. A 20-second hook is something people are tempted to skip with git commit --no-verify, while four seconds is usually short enough to leave enabled. That is where I think the real value of faster tooling shows up. Not in the benchmark number itself, but in the fact that the tool stops interrupting the development process.
The order I would migrate in
I would not change the entire toolchain on the same day. On the TypeScript 7 side, I would first clean up the deprecations on TypeScript 6, then run TypeScript 7 in CI with tsc --noEmit. After that I would check the editor and build system, and then audit anything that imports TypeScript directly. If one of those tools does not support TypeScript 7 yet, I would wait for an update or use the compatibility approach recommended by that tool. The goal is not to have every dependency on TypeScript 7 immediately; the goal is to upgrade the compiler without breaking the rest of the build system.
I am much more comfortable moving quickly with Oxlint. I would install it, add it to CI, and leave ESLint in place. For a while both run together. Then I would disable the rules in ESLint that Oxlint already covers, enable type-aware linting, and finally look at which ESLint plugins and rules are still actually needed. This turns the migration into a series of small changes rather than one large switch.
You do not have to replace everything
The release of TypeScript 7 does not mean every existing project needs to upgrade immediately. The same applies to Oxlint and Oxfmt.
For a new project, TypeScript 7 and Oxlint make a lot of sense to me. I would consider Oxfmt depending on the project's needs and how comfortable the team is with using a beta formatter. For a large existing codebase, I would move more slowly:
flowchart TD
a1["TypeScript 6"] --> a2["clean up deprecations"]
a2 --> a3["TypeScript 7 in CI"]
a3 --> a4["editor and build check"]
a4 --> a5["TypeScript 7"]
b1["ESLint"] --> b2["add oxlint alongside"]
b2 --> b3["disable overlaps"]
b3 --> b4["type-aware linting"]
b4 --> b5["trim ESLint"]
The left column is the compiler side, the right one is linting. Neither has to wait for the other.
The advantage of doing it this way is simple: when something breaks, you know which change caused it.
The bigger picture
The JavaScript and TypeScript toolchain is changing in a fairly fundamental way. The TypeScript compiler is moving to Go, Oxlint is written in Rust, and type-aware linting can use the TypeScript compiler infrastructure. Oxfmt is a Rust-based formatter trying to become a serious alternative to Prettier.
These tools are not really competitors. They are different pieces of the same toolchain: TypeScript 7 handles the compiler and type checking, Oxlint handles linting, tsgolint bridges the two for type-aware rules, and Oxfmt handles formatting. The change underneath all of this is bigger than "Rust is faster" or "Go is faster."
The short version of everything above:
| Situation | First step | Alternatives and notes |
|---|---|---|
| Speeding up linting on an existing project | Add oxlint next to ESLint | Keep ESLint, disable overlaps with eslint-plugin-oxlint |
You need rules like no-floating-promises |
oxlint --type-aware |
typescript-eslint (slower but established); tidy tsconfig first |
| Upgrading from TypeScript 5 | TypeScript 6 first, then 7 | Jumping straight to 7 mixes up where errors come from |
| A tool uses the compiler API | Wait for its TypeScript 7 support | Whatever compatibility path that tool documents |
| Changing formatters | Stay on Prettier for now | Try oxfmt on a branch and read the diff |
| Starting a new project | TypeScript 7 + oxlint | Judge oxfmt against its beta status |
| CI runs too long | Split lint, type check and format into steps | Separate jobs on a big repo, overkill on a small one |