Why I Stopped Using ESLint for TypeScript — And Started Using Biome

📅 May 7, 2026
Why I Stopped Using ESLint for TypeScript — And Started Using Biome
👁 ... views

A split visual showing ESLint and Prettier logos being replaced by the Biome logo, with performance gauges showing 30s vs 1.8s

Every time someone posts about replacing ESLint, the comments are a bloodbath. “ESLint is the industry standard.” “Nothing can replace its plugin ecosystem.” “You’ll regret it when you need a custom rule.”

I get it. I’ve been on that side of the argument. For years, ESLint was the one tool I’d install in every new project before writing a single line of code. It was non-negotiable — like Docker health checks or PostgreSQL for your data layer.

But something changed. And it wasn’t just the speed.

The Slow Death of My ESLint Config

I maintain about 12 TypeScript projects — a mix of internal tools, client work, and this blog’s infrastructure. Each one had its own eslint.config.js (flat config, because .eslintrc is dead now), plus prettier.config.js, plus a lint-staged setup to run both on pre-commit hooks.

The problem wasn’t the tools themselves. ESLint is powerful. Prettier is reliable. The problem was the maintenance tax.

Every time I upgraded a dependency, something broke. @typescript-eslint v7 wanted a different parserOptions.project structure. eslint-plugin-react-hooks needed a separate version pin. Prettier v3 changed how it resolved config in monorepos. And God forbid you try to share a base config across projects — the transitive dependency conflicts are a special kind of nightmare.

Then TypeScript 6 dropped with --erasableSyntaxOnly, and I started thinking about the whole toolchain differently. If my types can run directly in Node without a build step (I wrote about this already), why am I still waiting 30 seconds for my linter and formatter to finish on every commit?

That’s when I looked at Biome.

What Is Biome, Really?

Biome is a single Rust binary that does linting, formatting, and import sorting for JavaScript, TypeScript, JSX, JSON, and CSS. One tool. One config file. No plugin ecosystem to manage. No version conflicts between your linter, formatter, and their shared dependencies.

The speed claim caught my attention first: a case study from fireup.pro showed pre-commit hooks dropping from 27 seconds to 1.8 seconds — a 92.6% reduction — in a large TypeScript monorepo. That’s not a marginal improvement. That’s the difference between staying in flow context and checking your phone.

But speed alone doesn’t convince me to switch. I’ve fast tools that produce garbage. I needed to know: does Biome actually catch the same problems ESLint does?

The Migration: Under an Hour, One Config File

Here’s what my old setup looked like. This is from a real project — a FastAPI backend with a TypeScript admin dashboard:

// eslint.config.js — before
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import prettierConfig from "eslint-config-prettier";

export default tseslint.config(
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    files: ["**/*.tsx"],
    plugins: { react, "react-hooks": reactHooks },
    rules: {
      ...react.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
      "@typescript-eslint/no-explicit-any": "warn",
    },
  },
  prettierConfig,
);

// prettier.config.js — separate file
export default {
  semi: true,
  singleQuote: true,
  trailingComma: "all",
  printWidth: 100,
};

// .lintstagedrc.js — yet another file
export default {
  "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
  "*.{js,json}": ["prettier --write"],
};

Three config files. Seven dependencies in devDependencies. Two tools that sometimes disagreed (hello, eslint-config-prettier just to silence conflicts).

Here’s what it became:

// biome.json — that's it
{
  "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
  "organizeImports": { "enabled": true },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "correctness": {
        "noUnusedVariables": "error",
        "noUnusedImports": "error"
      },
      "suspicious": {
        "noExplicitAny": "warn"
      },
      "style": {
        "useImportType": "error"
      }
    }
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 100
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single",
      "semicolons": "always",
      "trailingCommas": "all"
    }
  }
}

One file. One dependency (@biomejs/biome). No config conflicts. No eslint-config-prettier glue layer.

The migration command was literally:

npx @biomejs/biome migrate eslint --write

Biome read my ESLint config and converted what it could. The rest I adjusted manually — about 15 minutes of tweaking. Then I ran:

npx biome check --write .

It linted, formatted, and sorted imports across 847 files in 2.3 seconds. My old eslint --fix && prettier --write pipeline took 31 seconds on the same codebase. Not a typo.

The Real Numbers: What I Measured

MetricESLint + PrettierBiome v2Change
Lint + format (847 files)31s2.3s13x faster
Pre-commit hook (staged files)8.4s0.6s14x faster
Config files to maintain3166% fewer
devDependencies for linting7186% fewer
CI time (lint step)45s4s11x faster

These aren’t cherry-picked numbers. This is from my actual monorepo on a 2024 MacBook Pro M3. The CI numbers are from GitHub Actions on their standard runner.

What struck me wasn’t just the raw speed — it was the consistency. With ESLint + Prettier, I’d occasionally get different results depending on which machine ran the lint. Node version differences, transitive dep resolution, cached parser state — the usual suspects. Biome is a single binary. Same output everywhere. Period.

Where Biome Falls Short (Because No Tool Is Perfect)

I’m not going to pretend Biome is a flawless replacement. Here’s what I genuinely miss from ESLint:

The plugin ecosystem. ESLint has 700+ rules across hundreds of plugins. Biome has a solid set of built-in rules (around 300+), but if you need something niche — like eslint-plugin-security or eslint-plugin-jest-dom — you’re out of luck. For my projects, Biome covered 95% of what I actually used. The remaining 5%? I could live without it.

Custom rules. If your team has 50+ custom ESLint rules encoding business logic or style decisions, Biome won’t help you yet. The Biome team is working on custom rule support, but it’s not here. Teams with heavy custom rule investment should stay on ESLint. I had exactly 3 custom rules, and all three were things I’d cargo-culted from a template years ago. None of them caught a real bug in production.

Type-aware linting. Biome does type-aware analysis for some rules, but TypeScript’s own tsc --noEmit still catches things neither Biome nor ESLint can — unreachable code from exhaustive switch statements, certain generic constraint violations. Biome complements tsc, it doesn’t replace it. My current setup is: Biome for lint + format + speed, tsc --noEmit for type-checking in CI.

Here’s the counter-argument table I always include when I write about switching tools:

ConcernReality
”ESLint is the industry standard”True. But “industry standard” often means “nobody questioned the default.” Standard doesn’t mean optimal.
”Biome doesn’t have plugins”True. But most projects cargo-cult 200+ ESLint rules they don’t need. Biome’s recommended rules cover the important ones.
”We have custom rules”If you have 50+, stay on ESLint. If you have 3 you copied from a template, audit them honestly. You might not need them.
”What about type checking?”Biome doesn’t replace tsc. Keep tsc --noEmit in CI. Biome replaces ESLint + Prettier, not the TypeScript compiler.
”Migration risk”Biome’s migrate eslint command handles most of it. Run it on a branch, diff the results, verify. It’s low-risk.

What I Actually Do Now

My current toolchain for new TypeScript projects:

# 1. Biome for linting, formatting, import sorting
npx biome check --write .

# 2. TypeScript compiler for type-checking (CI only)
npx tsc --noEmit

# 3. That's it

For existing projects with heavy ESLint customization, I don’t recommend a blind migration. Audit your rules first. See what you actually use. The npx biome migrate eslint --write command will show you what maps and what doesn’t. If 80%+ maps cleanly, it’s worth the switch. If not, stay put.

One thing I discovered during the migration: Biome’s check command combines linting and formatting into a single pass. In my pre-commit hooks, I replaced this:

# Old: two passes, two tools
eslint --fix --staged && prettier --write --staged

With this:

# New: one pass, one tool
biome check --write --staged

That’s not just faster — it’s simpler. And simplicity compounds. Every new developer on the team needs to learn one tool instead of two. Every CI pipeline has one fewer step. Every dependency conflict disappears.

The Honest Verdict

If you’re starting a greenfield TypeScript project in 2026, I’d use Biome without hesitation. It’s fast, it’s simple, it catches the important problems, and the config fits in a single file you can actually read without scrolling.

If you’re maintaining a large codebase with deep ESLint customization — custom rules, 15 plugins, team-specific standards — the migration cost probably outweighs the speed benefit. Stay on ESLint. It works. Upgrade to flat config if you haven’t already.

For everything in between — which is most projects — I think the speed and simplicity trade-off is worth it. My pre-commit hooks went from 8.4 seconds to 0.6 seconds. I’m not getting that time back from ESLint.

And honestly? After years of debugging why ESLint behaves differently on CI than locally, having a single Rust binary that produces identical output everywhere is worth more than I expected. Sometimes the best tool is the one you stop thinking about.


This article is part of my TypeScript deep-dive series. If you haven’t read it yet, check out TypeScript 6 Erased My Build Step — it’s the companion piece on why the entire TypeScript toolchain is getting simpler in 2026.

🛠️ Level Up Your TypeScript Game

If you're serious about TypeScript in 2026, these resources helped me most:

  • Biome Documentation: biomejs.dev — Official docs, migration guide, and rule reference. Free and open-source.
  • Effective TypeScript: Dan Vanderkam's book — 62 specific ways to write better TypeScript. Still the best TS book after 5 re-reads.
  • TypeScript Deep Dive: basarat.gitbook.io — Free online resource covering everything from basics to advanced patterns.

Some links above are affiliate links. They cost you nothing and support this blog.

💡

Enjoying the content? Here are tools I personally use and recommend:

  • 🌐 Hosting: Bluehost — what this blog runs on
  • 🛒 Tech Gear: My Amazon Store — keyboards, monitors, dev tools I use

Purchases through my links help keep this blog ad-free 💙

Enjoyed this post?

Subscribe to the newsletter or follow on YouTube for more dev content.

🎬 Watch Shorts