DEV Community

Cover image for Biome: How to Setup Go-Level Linting and Formatting in Your Typescript
Nahwin Rajan
Nahwin Rajan

Posted on Originally published at spectredev.xyz AI-assisted

Biome: How to Setup Go-Level Linting and Formatting in Your Typescript

Originally published at spectredev.xyz. Cross-posted here for the Dev.to community.

Biome replaces ESLint and Prettier with one fast binary. Here's the exact setup for TypeScript projects: config, rules, and VSCode integration.


Go ships with gofmt. You run it, your code is formatted. No config, no plugins, no arguments about tabs vs spaces. The entire Go ecosystem shares one format because the tool makes the decision for you.

TypeScript doesn't have that. Or didn't. Biome is the closest thing to it: a linter and formatter in a single binary that runs fast enough that you stop noticing it's running, and opinionated enough that formatting debates stop happening.

This is the setup that gets you there.


Why ESLint + Prettier Is a Liability at Scale

The combination works. The combination is also a maintenance surface you're constantly managing.

ESLint has a plugin ecosystem of 2,000+ packages. Most projects use six to twelve of them. Each plugin is a separate dependency with its own release cycle, its own compatibility requirements, and its own bugs. eslint-config-airbnb pulls in seven packages. @typescript-eslint/eslint-plugin pulls in three. When ESLint releases a major version, you wait for all your plugins to catch up.

Prettier is separate configuration, separate package, separate invocation. If ESLint has formatting rules that conflict with Prettier — and it does, by default — you add eslint-config-prettier to disable them. One more package. One more thing to update.

The result is a lint/format setup that takes 30–60 seconds on a large project, breaks every few months when dependencies fall out of sync, and requires a senior engineer to diagnose when CI fails because @typescript-eslint/parser doesn't support the ESLint version you installed.

Biome collapses all of this into one binary, one config file, one command, and sub-second execution on most codebases.


Installation

bun add --dev @biomejs/biome
Enter fullscreen mode Exit fullscreen mode

Or with npm:

npm install --save-dev @biomejs/biome
Enter fullscreen mode Exit fullscreen mode

Initialize the config:

bunx biome init
Enter fullscreen mode Exit fullscreen mode

This creates biome.json in the project root. That's the only config file you'll need.


The Config

Start here. This is the setup that works for production TypeScript projects without modification:

{
  "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
  "vcs": {
    "enabled": true,
    "clientKind": "git",
    "useIgnoreFile": true
  },
  "files": {
    "ignoreUnknown": false,
    "ignore": ["dist", "node_modules", "coverage", ".next"]
  },
  "formatter": {
    "enabled": true,
    "formatWithErrors": false,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineEnding": "lf",
    "lineWidth": 100,
    "attributePosition": "auto"
  },
  "organizeImports": {
    "enabled": true
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "correctness": {
        "noUnusedImports": "error",
        "noUnusedVariables": "error",
        "useExhaustiveDependencies": "warn"
      },
      "suspicious": {
        "noExplicitAny": "error",
        "noConsole": "warn"
      },
      "style": {
        "noVar": "error",
        "useConst": "error",
        "useTemplate": "error"
      },
      "complexity": {
        "noForEach": "warn"
      }
    }
  },
  "javascript": {
    "formatter": {
      "jsxSingleQuote": false,
      "quoteStyle": "double",
      "semicolons": "always",
      "trailingCommas": "all"
    },
    "globals": []
  },
  "typescript": {
    "formatter": {
      "quoteStyle": "double"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

A few rules worth calling out.

"organizeImports": { "enabled": true } auto-sorts imports on format. No more import order lint errors. Biome handles it.

noExplicitAny: "error" will break codebases that have used any liberally. Don't set this to "warn" as a compromise that just means you accumulate warnings forever. Either set it to "error" and fix the underlying types, or don't enable it. Half-measures here are noise.

noConsole: "warn" catches console.log statements left in production code. Set it to "error" if you want CI to fail on them. We use "warn" because there are legitimate uses for console output in scripts and CLI tools.

useTemplate: "error" enforces template literals over string concatenation. `Hello ${name}` instead of "Hello " + name. Small thing, but consistent.


Running Biome

# Check everything (lint + format check, no changes)
bunx biome check .

# Fix everything that can be auto-fixed
bunx biome check --write .

# Format only
bunx biome format --write .

# Lint only
bunx biome lint .
Enter fullscreen mode Exit fullscreen mode

Add to package.json:

{
  "scripts": {
    "check": "biome check .",
    "check:fix": "biome check --write .",
    "format": "biome format --write ."
  }
}
Enter fullscreen mode Exit fullscreen mode

CI should run biome check . without --write. If there are formatting violations, CI fails. Engineers fix locally with biome check --write . before pushing.


The Part Most Teams Get Wrong

They install Biome and keep ESLint running in parallel "just to be safe."

I understand the instinct. ESLint has rules you've relied on for years. Some of them feel important. But running both creates a situation where engineers never fully trust Biome, so the ESLint config never gets removed, and you end up with two tools providing overlapping feedback that occasionally conflicts.

Pick one. If you want Biome, commit to it. Audit which ESLint rules you actually care about, confirm Biome covers them (the migration guide maps ESLint rules to Biome equivalents), and delete the ESLint config.

The one legitimate exception: a rule your project genuinely needs that Biome doesn't have. In that case, keep ESLint for that specific rule only, with a minimal config that does nothing else. Don't keep the full ESLint setup.

The other mistake: using biome-ignore comments the same way teams used eslint-disable. One biome-ignore with a real explanation is fine. Thirty of them means you're configuring around the tool instead of using it.


Real-World Example: Linear's Tooling Philosophy

Linear the project management tool used by thousands of engineering teams has written publicly about their TypeScript setup. One consistent thread: they treat formatting and linting as non-negotiable infrastructure. The config is decided once, applied everywhere, and the only valid PR comment about formatting is "Biome should have caught this is it not running?"

The teams we've worked with that adopted this stance saw a specific change: code review comments shifted from formatting and style discussions to architecture and logic discussions. Not because engineers got more disciplined. Because the tool made the trivial decisions automatic.

A two-engineer team running ESLint + Prettier was spending roughly 90 minutes per week in PRs on formatting and import order. After switching to Biome with format-on-save in VSCode, that dropped to near zero. Not because they formatted better — because the tool formatted for them on every save.


VSCode Setup

Install the official Biome extension: biomejs.biome.

Add to .vscode/settings.json:

{
  "editor.defaultFormatter": "biomejs.biome",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.organizeImports.biome": "explicit",
    "quickfix.biome": "explicit"
  },
  "[typescript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[javascript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[json]": {
    "editor.defaultFormatter": "biomejs.biome"
  }
}
Enter fullscreen mode Exit fullscreen mode

Commit .vscode/settings.json. Every engineer on the team gets the same format-on-save behaviour without manual configuration.

Disable the Prettier VSCode extension for this project. They'll conflict, and Biome will win in CI while Prettier wins in the editor, which means code that looks right locally fails CI. Avoid the confusion.


FAQ

Q: Does Biome support JSX and React?
A: Yes. JSX formatting and linting work out of the box. The useExhaustiveDependencies rule catches missing React hook dependencies, similar to eslint-plugin-react-hooks. No separate plugin installation required.

Q: Can Biome lint JSON and YAML files?
A: JSON, yes. YAML is not supported as of Biome 1.9. If you have YAML linting requirements, you'll need a separate tool for those files specifically.

Q: How does Biome handle .env files and secrets?
A: It doesn't that's not its job. Biome lints and formats source code. For secret detection in commits, use a separate tool like gitleaks or GitHub's built-in secret scanning. Don't expect Biome to catch hardcoded API keys.

Q: Is Biome stable enough for production CI?
A: Yes. Biome 1.x has been in production at companies including Netlify and Astro's own codebase. The 1.x series follows semver — breaking changes don't happen in minor releases. Pin to a specific minor version in package.json and update intentionally.

Q: What happens when Biome and tsconfig disagree on something?
A: They operate on different layers and rarely conflict. Biome checks code style and correctness patterns. TypeScript checks types. The one overlap: both care about noUnusedLocals and import usage. If both are enabled, you'll see the error from both. That's redundant but not harmful; you can disable the TypeScript compiler option and let Biome handle it.


One config file. One command. No plugin dependency hell. The goal isn't to make your linter fancy — it's to make it invisible so you can think about the actual code.


Further Reading

Top comments (0)