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.
| Dimension | Biome 2.3+ | ESLint 10 + typescript-eslint 8.59 |
|---|---|---|
| Type-aware linting | Yes, without tsc in the loop | Yes, via the TS compiler (slower on large repos) |
| Framework-specific plugin coverage | Partial | Full (react-hooks, next, jsx-a11y) |
| Custom rule authoring | Immature plugin system | Mature, RuleTester-based |
| Single-pass format+lint | Yes | No (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 excludesuseEffectEventfunctions from exhaustive-deps warnings (a real false-positive class in v5 and earlier onceuseEffectEventshipped in React 19.2). If a codebase pinseslint-plugin-react-hooks@5alongside React 19.2+, expect spuriousexhaustive-depswarnings on everyuseEffectEventcall site.@biomejs/biome@>=2.3for type-aware rules; anything1.xpredates Biotype and has no type inference at all - treat1.xconfigs 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)
| Construct | Convention | Example |
|---|---|---|
| Variables, functions | camelCase | getUserById |
| Classes, types, interfaces, enums | PascalCase | UserRepository |
| Constants (module-level, immutable) | UPPERSNAKECASE | MAXRETRYATTEMPTS |
| File names (non-component) | kebab-case.ts | user-repository.ts |
| React component files | PascalCase.tsx | UserProfile.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@<6on a React 19.2+ codebase - produces false-positiveexhaustive-depswarnings onuseEffectEvent, 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 arbitrarypostinstallcode 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.*andeslint.config.jsside 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.