languages/javascript/code_style_guide.md

JavaScript/TypeScript Code Style Guide (2026)

Agent-facing linter/formatter decision guidance for 2026: Biome 2.x's type-aware linting (no TS compiler needed), current ESLint/typescript-eslint/eslint-plugin-react-hooks versions, and when ESLint's plugin ecosystem still beats Biome.

Assumes the reader knows what a linter and a formatter are. This is the current decision, not the definitions.

The Biome-vs-ESLint gap closed in 2025 - update any prior "Biome lacks type-aware linting" claim

Biome 2.0 ("Biotype", late 2025) shipped type-aware linting without invoking the TypeScript compiler - it builds its own type inference layer instead of shelling out to tsc, which is precisely the performance bottleneck that made typescript-eslint's recommendedTypeChecked configs slow on large codebases. Biome 2.3 (January 2026) covers 491 lint rules. Weekly npm downloads for @biomejs/biome were ~8.8M as of mid-May 2026. In production at Vercel, Discord, Slack, Coinbase, Astro, and the Node.js project itself.

What Biome still doesn't have, as of 2026: a mature third-party plugin ecosystem for framework-specific rules and custom domain rules - eslint-plugin-react-hooks, @next/eslint-plugin-next, and hand-written organization-specific ESLint rules have no Biome equivalent. That, not "no type awareness," is the real remaining reason to stay on ESLint.

DimensionBiome 2.3+ESLint 10 + typescript-eslint 8.59
Type-aware lintingYes, without tsc in the loopYes, via the TS compiler (slower on large repos)
Framework-specific plugin coveragePartialFull (react-hooks, next, jsx-a11y)
Custom rule authoringImmature plugin systemMature, RuleTester-based
Single-pass format+lintYesNo (two tools)

Verdict: default to Biome for new projects. Stay on ESLint + typescript-eslint only where eslint-plugin-react-hooks or another framework-specific/custom rule set is load-bearing.

Current version floor - pin these, not older majors

  • eslint@>=10, @eslint/js@>=10, typescript-eslint@>=8.59 - flat config (eslint.config.js) is the only format ESLint 10 ships; .eslintrc.* is dead weight to delete, not migrate.
  • eslint-plugin-react-hooks@>=6 - defaults to flat config and, notably, correctly excludes useEffectEvent functions from exhaustive-deps warnings (a real false-positive class in v5 and earlier once useEffectEvent shipped in React 19.2). If a codebase pins eslint-plugin-react-hooks@5 alongside React 19.2+, expect spurious exhaustive-deps warnings on every useEffectEvent call site.
  • @biomejs/biome@>=2.3 for type-aware rules; anything 1.x predates Biotype and has no type inference at all - treat 1.x configs as needing a real migration, not a version bump.
// biome.json - type-aware rule now available without a tsc dependency
{
  "$schema": "https://biomejs.dev/schemas/2.3.0/schema.json",
  "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2 },
  "linter": {
    "enabled": true,
    "rules": { "recommended": true, "suspicious": { "noExplicitAny": "error" } }
  },
  "javascript": { "formatter": { "quoteStyle": "double", "semicolons": "asNeeded" } }
}
// eslint.config.js - flat config, react-hooks v6, type-aware
import js from "@eslint/js"
import tseslint from "typescript-eslint"
import reactHooks from "eslint-plugin-react-hooks"

export default tseslint.config(
  js.configs.recommended,
  ...tseslint.configs.recommendedTypeChecked,
  {
    files: ["**/*.{ts,tsx}"],
    languageOptions: { parserOptions: { project: "./tsconfig.json" } },
    plugins: { "react-hooks": reactHooks },
    rules: {
      "react-hooks/rules-of-hooks": "error",
      "react-hooks/exhaustive-deps": "warn",
      "@typescript-eslint/no-floating-promises": "error",
    },
  },
)

Naming conventions (unchanged, enforce via lint rule not review)

ConstructConventionExample
Variables, functionscamelCasegetUserById
Classes, types, interfaces, enumsPascalCaseUserRepository
Constants (module-level, immutable)UPPERSNAKECASEMAXRETRYATTEMPTS
File names (non-component)kebab-case.tsuser-repository.ts
React component filesPascalCase.tsxUserProfile.tsx
Private class members#field (native private)#connectionPool

Enforce via @typescript-eslint/naming-convention or Biome's equivalent - not code review; humans miss it consistently.

Anti-patterns worth flagging specifically

  • Pinning eslint-plugin-react-hooks@<6 on a React 19.2+ codebase - produces false-positive exhaustive-deps warnings on useEffectEvent, training developers to silence real warnings alongside the fake ones.
  • Treating install-time devDependency install as automatically safe - as of npm 12 / pnpm 10+ install scripts are deny-by-default (see README.md's supply-chain section), but a lockfile pinned to an older package-manager major still executes arbitrary postinstall code from any transitive dependency. A linter/formatter choice is itself a supply-chain decision: fewer devDependencies (Biome: one package) is a smaller attack surface than ESLint+Prettier's typical dozen-plus plugin packages.
  • **Running .eslintrc.* and eslint.config.js side by side "during migration" indefinitely** - ESLint 10 has fully dropped the legacy cascade; a stale .eslintrc.* left in the tree is dead config, not a fallback.

Sources