Run typescript-eslint over a real monorepo with typed rules on and you wait. Not seconds. On a mid-sized codebase the cold lint can stretch past seven minutes, and almost all of that time is spent inside one place: the TypeScript type checker, answering questions like 'is this promise awaited?' for every node in the tree.

In June 2025, two Rust-based linters hit 1.0 within a week of each other. Both want to retire ESLint. Both are fast enough that the seven-minute lint becomes a sub-two-second one. And both then walk straight into the same wall typescript-eslint hit years ago: the rules people actually fear breaking production need types, and getting types is slow. What separates them is how they decided to pay for it.

Two kinds of lint rule, two completely different costs

Most lint rules are cheap. A rule like no-debugger or react-hooks/rules-of-hooks only needs the abstract syntax tree of a single file. Parse the file, walk the nodes, flag the pattern. There is no cross-file work and no semantic question to answer, so each file is independent. That property is what makes a Rust linter look magical: independent work parallelizes across cores, and a compiled language with no per-file V8 warmup chews through it.

Type-aware rules are a different animal. Consider no-floating-promises, the rule that catches an async call whose returned promise is never awaited or handled. To know whether an expression is a promise, the linter has to resolve its type. That means building a TypeScript Program, loading every file the target imports, and calling the compiler's type-checking APIs. The checker is single-threaded JavaScript, it is the slow part of tsc itself, and a handful of these rules dominate the entire lint run.

The payoff is real, which is why teams tolerate the cost. This is the kind of bug only a type-aware rule sees:

async function chargeCard(id: string): Promise<Receipt> {
  // ...
}

async function checkout(cart: Cart) {
  validate(cart);
  chargeCard(cart.userId); // returns a Promise, never awaited
  return { ok: true };     // we return before the charge resolves or rejects
}

A purely syntactic rule cannot flag the chargeCard line. The call looks identical to a synchronous one. Only by resolving the return type to a Promise does the linter know the result is being dropped on the floor, which means a rejected charge becomes an unhandled rejection and the function reports success anyway. Catching that class of bug is the whole reason typed linting exists, and it is why nobody wants to give up the typed rules to go faster.

typescript-eslint has spent years optimizing around this. Its newer Project Service API reuses TypeScript's project-loading machinery so monorepos do not reload a program per file, and it gets close to editor-grade incremental behavior. It is still the same checker underneath. The team's own guidance is blunt about the trade: typed linting is worth it for the bugs it catches, and you run it in CI rather than on every keystroke because you cannot make the checker free.

So the real question for any challenger is not 'can you lint faster.' Everyone can lint the syntactic rules faster than ESLint. The question is what you do about the rules that need a type checker. Oxlint and Biome answered it in opposite ways.

Oxlint's bet: borrow the real compiler

Oxlint comes out of the oxc project and VoidZero, the same Rust-tooling effort behind Rolldown. It reached 1.0 on June 10, 2025, led by Boshen with a full-time maintainer and over 200 contributors. The pitch is unsubtle: 50 to 100 times faster than ESLint on the same configuration. One published benchmark clocks a multi-threaded Oxlint run at 615 milliseconds against ESLint's 33.5 seconds. The largest internal test linted 264,925 files with 101 rules across 10 threads in 22.5 seconds.

Those are not just lab numbers. Airbnb reported running multi-file rules across more than 126,000 files in seven seconds, a job ESLint could not finish before timing out. Mercedes-Benz measured a 71% drop in lint time, with some projects closer to 97%. Shopify runs it in its admin console; Bun and Preact lint with it too.

At 1.0, Oxlint shipped over 500 ported rules covering ESLint core, the typescript-eslint syntactic rules, and popular plugins like unicorn, jsdoc, react, react-hooks, jest, and import. That count is past 700 now. The config file, .oxlintrc.json, mirrors ESLint's shape closely enough that migration is mostly mechanical.

None of that touches the hard part. For type-aware rules, Oxlint made a telling choice: it does not implement type inference at all. It shells out to a separate Go binary called tsgolint, which builds TypeScript programs on top of typescript-go, the official Go port of the compiler that ships as TypeScript 7. Oxlint handles file traversal, config, and the cheap rules in Rust; tsgolint builds the program, runs the typed rules against the genuine TypeScript type system, and hands structured diagnostics back.

The strategy is to let someone else own correctness. tsgolint does not guess at types. It calls the same checker Microsoft maintains, through a shim that exposes the compiler's internal APIs. The progression has been quick. A technical preview landed mid-2025; the December 8, 2025 alpha covered 43 of typescript-eslint's type-aware rules and clocked 8x faster than typescript-eslint on vuejs/core and 12x on outline/outline. The current docs put coverage at 59 of 61 rules. You turn it on with a flag:

# type-aware rules run through tsgolint on typescript-go
oxlint --type-aware

# or pin it in .oxlintrc.json
# { "options": { "typeAware": true } }

The catch is in the dependency. Type-aware Oxlint needs TypeScript 7.0+, so it inherits the whole TypeScript 7 migration story. Some legacy tsconfig options are unsupported, monorepos need their declaration files present before a run, and very large codebases can still hit high memory use. It is not a free upgrade. It is a bet that the official compiler, rewritten in Go, gets fast enough to make typed linting cheap, and that aligning with it beats reimplementing it.

Isometric tech illustration of a turbine powering an amber engine.

The plugin gap is closing faster than expected

The other thing keeping teams on ESLint was custom rules. A large codebase usually carries a few internal rules and a long tail of community plugins, and a linter that cannot run them is a non-starter. Oxlint's answer arrived as a JavaScript-plugin alpha on March 11, 2026, with an ESLint v9-compatible plugin API. Existing plugins are meant to run unmodified, and you can write new rules in JS or TypeScript and still get autofixes and editor diagnostics.

VoidZero's framing is that roughly 80% of ESLint users can now switch and have it 'just work.' One benchmark with JS plugins enabled finished 6,298 files in 21 seconds against ESLint's 1 minute 43, a 4.8x gap. That is a smaller multiple than the headline 50-100x, which is the honest number: the moment you run JavaScript rules, you pay for a JavaScript runtime, and the Rust advantage narrows. It is still meaningfully faster, and it removes the last hard blocker.

Biome's bet: synthesize the types yourself

Biome took the opposite road. Version 2.0 shipped on June 17, 2025, under the codename Biotype, and it claims a first: type-aware lint rules that do not rely on the TypeScript compiler at all. You can run type-informed linting without even installing the typescript package.

It works through a file scanner. Before 2.0, Biome rules could only see one file at a time. The scanner now indexes the project, similar to how an editor language server builds a model of your code, so a rule can resolve a type imported from another module. Biome then runs its own type inference over that index. There is no tsc, no Go port, no second process. One Rust binary does linting, formatting, and now type-aware analysis.

That unification is Biome's real selling point, and it is easy to undersell. Biome already replaces both ESLint and Prettier with a single tool and a single config. Folding type-aware rules into that same binary means one dependency, one pass, one mental model for the whole format-and-lint step. Oxlint, by contrast, is a linter; formatting lives in a separate, younger oxc tool.

The cost shows up in coverage. Biome's flagship typed rule, noFloatingPromises, catches roughly 75% of the cases typescript-eslint would, at a fraction of the performance impact. The team is candid that mileage varies and the type system is young. To keep the scanner's cost contained, Biome puts these rules behind a project domain and never recommends them by default, and the full project-plus-node_modules scan only runs when project rules are enabled.

Biome's plugin story is earlier than Oxlint's. The 2.0 plugin system is a first iteration built on GritQL pattern matching: it can match code snippets and report diagnostics, but it is not the full programmable rule API that ESLint and now Oxlint expose. For teams whose custom rules are simple structural checks, that is enough. For teams with logic-heavy internal rules, it is not yet.

Isometric tech illustration of a glowing turbine in a glass dome.

Where the two strategies actually diverge

Strip away the speed marketing and the difference is philosophical. Oxlint outsources the hard problem to the official compiler and inherits its correctness for free, at the price of a heavier dependency and a separate process. Biome owns the entire stack and ships one fast binary, at the price of maintaining a bespoke type system that has to chase TypeScript's behavior forever.

That second cost is the one critics keep pointing at. A hand-written inference engine cannot guarantee full coverage or behavioral alignment with the official compiler, a concern Vite's creator Evan You has raised publicly and one the Biome team does not dispute. Every TypeScript release that changes inference is work Biome has to mirror. Oxlint gets that alignment by construction, because it is calling the real thing.

Here is the landscape as it stands:

Dimensiontypescript-eslintOxlintBiome
EngineJS, on top of tscRust + tsgolint (Go) on typescript-goRust, own type inference
Syntactic-rule speedBaseline50-100x fasterComparable to Oxlint
Type-aware rulesFull, mature (the reference set)59 of 61 ported, alphaA handful, ~75% recall on the flagship
Alignment with TS semanticsExact (uses tsc)Exact (uses the official Go compiler)Approximate (bespoke engine)
Custom plugin APIFull, huge ecosystemESLint v9-compatible, alphaGritQL pattern-match, report-only
Formatter includedNo (pair with Prettier)Separate oxc toolYes, one binary
Hard dependencyNode + tscTypeScript 7.0+ for typed rulesNone for typed rules

What to actually do in CI right now

The honest reading is that the syntactic war is over and the typed war is not. For the cheap rules, there is no good reason to keep paying ESLint's startup tax. For the typed rules, typescript-eslint is still the only fully mature option, and both challengers are explicit that their typed modes are young.

That points at a two-pass setup, which is where most teams are landing:

  • Run the fast syntactic linter on every save and in pre-commit. Oxlint or Biome both do this in well under a second, so the editor feedback loop stays tight and pre-commit stops being the step people skip.
  • Keep the type-aware pass in CI, where a slower run is acceptable. Today that is typescript-eslint. As tsgolint stabilizes, Oxlint's typed mode is the natural drop-in because the rules and the semantics are the same ones.
  • Treat the challengers' typed modes as something to pilot, not depend on. 59 of 61 rules in alpha is close, but 'alpha' and 'high memory on large monorepos' are not words you want in a blocking CI gate yet.

Migration itself is cheap to try. Oxlint ships @oxlint/migrate for ESLint flat configs, and Biome has biome migrate eslint --write. Both read your existing rules and translate what they can, then tell you what they dropped. The realistic friction is not the config. It is your custom rules and the long tail of plugins, which is exactly the gap Oxlint's plugin alpha and Biome's GritQL plugins are racing to close.

If you live entirely inside one toolchain and value a single binary over everything else, Biome is the cleaner story today. If your real concern is that typed rules behave exactly like the compiler your team already trusts, Oxlint's plumbing is the safer long-term bet. Neither argument is settled, and that is the interesting part.

The reimplementation underneath it all

There is a tidy way to see the whole contest. typescript-eslint's moat was never the linter. It was 59 type-aware rules refined against years of real-world TypeScript edge cases. Both challengers are now copying that exact rule set onto a faster engine, which tells you the rules were the valuable thing all along.

The split is in what each one decided to reimplement. Oxlint reimplemented the linter and let Microsoft keep the type checker. Biome reimplemented the type checker too. The first bet wins if typescript-go gets fast enough that calling the official compiler is no longer the bottleneck, which is the entire premise of TypeScript 7. The second wins if a bespoke engine can stay close enough to official behavior that the difference never bites a real codebase.

Watch one number over the next year: how close Oxlint's typed mode gets to typescript-eslint's correctness while staying fast. If it lands, the long-standing trade between safety and speed in linting quietly disappears, and the only open question left is whose binary you run.