# ESLint

ESLint matches source against a rule set written over the syntax tree: unused
bindings, shadowed names, unreachable code. Given type information it reaches
further — floating promises, `any` values flowing into places that assume something
narrower.

This page targets **ESLint 9** (flat config) with **TypeScript** via
`typescript-eslint`. Every rule choice comes from the typescript-eslint
maintainers' canonical position (typescript-eslint maintainers n.d.)[^typescripteslintnd]:

> Lint rules split into **Logical** (real verification),
> **Stylistic** (conventions), and **Formatting** (whitespace).
> Only the _logical_ subset is verification work. Hand
> formatting to a formatter; hand stylistic conventions to a
> formatter where possible.

## Setup

```bash
npm install --save-dev eslint typescript-eslint @eslint/js
```

A Prettier-style formatter is a separate dependency
(`npm install --save-dev prettier`) and lives in a separate
config (`.prettierrc.json`). The ESLint config here assumes
Prettier handles formatting; ESLint handles logic.

## Recommended `eslint.config.js` baseline

```js
// eslint.config.js — ESLint 9 flat config
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import globals from "globals";

export default tseslint.config(
  // Files to lint.
  {
    files: ["src/**/*.{ts,tsx,js,jsx}", "tests/**/*.{ts,js}"],
    ignores: ["dist/**", "build/**", "coverage/**", "**/*.d.ts"],
  },

  // ESLint core: logical rules only.
  js.configs.recommended,

  // typescript-eslint: type-checked recommendations.
  ...tseslint.configs.recommendedTypeChecked,
  ...tseslint.configs.stylisticTypeChecked,

  // Project-wide overrides.
  {
    languageOptions: {
      parserOptions: {
        projectService: true, // ESLint 9 type-aware
        tsconfigRootDir: import.meta.dirname,
      },
      globals: { ...globals.browser, ...globals.node },
    },
    rules: {
      // ===== Logical rules worth keeping =====
      "@typescript-eslint/no-floating-promises": "error",
      "@typescript-eslint/no-misused-promises": "error",
      "@typescript-eslint/await-thenable": "error",
      "@typescript-eslint/no-unnecessary-condition": "warn",
      "@typescript-eslint/switch-exhaustiveness-check": "error",
      "@typescript-eslint/no-unused-vars": [
        "error",
        { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
      ],

      // ===== Stylistic rules we keep (small, named set) =====
      "@typescript-eslint/consistent-type-imports": "error",
      "@typescript-eslint/array-type": ["error", { default: "array" }],

      // ===== Disabled — formatting belongs to Prettier =====
      "@typescript-eslint/quotes": "off",
      "@typescript-eslint/semi": "off",
      "@typescript-eslint/indent": "off",
      "@typescript-eslint/comma-dangle": "off",
    },
  },

  // Test-file relaxations.
  {
    files: ["tests/**/*", "**/*.test.ts", "**/*.spec.ts"],
    rules: {
      "@typescript-eslint/no-explicit-any": "off",
      "@typescript-eslint/no-non-null-assertion": "off",
    },
  },
);
```

### typescript-eslint

- **`@typescript-eslint/no-floating-promises`**. A `Promise`
  whose result is never awaited is almost always a bug — the
  caller has lost the error path, the value, or both.
- **`@typescript-eslint/no-misused-promises`**. An `async`
  function passed where a sync callback is expected — `if (asyncFn())`
  tests a promise object, which is always truthy.
- **`@typescript-eslint/await-thenable`**. `await` on a value
  that isn't a promise. The cause is usually a missing call on
  the function that was meant to be awaited.
- **`@typescript-eslint/switch-exhaustiveness-check`**. Adding
  a new sum-type variant and forgetting to handle it in one of
  the existing `switch` statements is exactly what types are
  supposed to catch.

### eslint-plugin-react-hooks

LLMs systematically violate React Hooks constraints.
`eslint-plugin-react-hooks` catches these cheaply at lint time.

### eslint-plugin-regexp

Catches regexp bugs LLMs write: duplicate alternatives,
character-class errors, and (via `no-super-linear-backtracking` /
`no-super-linear-move`) a cheap static heuristic for super-linear
backtracking. That heuristic is a first pass, not a real ReDoS check:
for that, [eslint-plugin-redos](https://github.com/makenowjust-labs/eslint-plugin-redos) (powered by
[recheck](https://makenowjust-labs.github.io/recheck/)) runs actual complexity analysis — sound but
costly (seconds per pattern, 10 s default timeout). See
[ReDoS detection](https://quality.stereobooster.com/deep-static-analysis.md#redos)
for the cross-language picture.

### Other plugins worth enabling

- **React** — [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react) and
  [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react) (the
  `eslint-react` project; correctness rules, not just style), plus
  [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) for
  accessibility.
- **Node** — [eslint-plugin-n](https://github.com/eslint-community/eslint-plugin-n): imports that won't
  resolve, Node built-ins/APIs missing from your supported `engines`,
  deprecated APIs — "will it actually run" checks.
- **Test runners** — official plugins for
  [Vitest](https://github.com/vitest-dev/eslint-plugin-vitest) (`@vitest/eslint-plugin`),
  [eslint-plugin-playwright](https://github.com/mskelton/eslint-plugin-playwright), and
  [eslint-plugin-testing-library](https://github.com/testing-library/eslint-plugin-testing-library).
- **Bug-finding** — [eslint-plugin-sonarjs](https://github.com/SonarSource/SonarJS)
  for its _logical_ rules (identical branches, identical conditions,
  redundant boolean logic — copy-paste and logic bugs). Its
  cognitive-complexity rule is a heuristic signal, not verification;
  treat it as advisory.

## Rules to skip / why

- **`max-len`**, **`max-lines`**, **`max-params`** — heuristics
  for human-readable code; Prettier covers most line-width
  questions. The other two trigger refactors that are usually
  worse than what they replaced.
- **`no-console`** as `error`. Use `warn`; lots of legitimate
  debug usage flows through `console`. The strict version
  produces "fix the lint" PRs that only add eslint-disable
  comments.
- **`prefer-const`** as `error` — fine as `warn`. As `warn` it
  surfaces; as `error` it forces churn.
- **All `airbnb-config`-style imports** — these mix logical,
  stylistic, and formatting rules under one umbrella, which is
  exactly what the trichotomy says not to do. Cherry-pick
  what you want.

## Formatting belongs to Prettier (or dprint)

A formatter is a pretty-printer: input → single canonical
output, deterministically, every time. ESLint's
_formatting_ rules duplicate that work, more slowly, with
configuration arguments. Hand whitespace, semicolons, quotes,
trailing commas, brace placement to Prettier or dprint.

Recommended `.prettierrc.json`:

```json
{
  "semi": true,
  "singleQuote": false,
  "trailingComma": "es5", // or "all" — the default in latest Prettier
  "printWidth": 80,
  "tabWidth": 2
}
```

`trailingComma: "es5"` reduces diff noise — adding a new entry
to an array or object doesn't show the previous line as modified
just to gain a trailing comma.

## Diff-time deployment, not nightly

The largest empirical finding on static-analysis adoption is that
**diff-time deployment beats batch deployment**; the evidence is on
[static analysis](https://quality.stereobooster.com/static-analysis.md).
Run ESLint as a PR-time gate, not as a nightly batch job that nobody
triages.

GitHub Actions example:

```yaml
# .github/workflows/lint.yml
name: lint
on:
  pull_request:

jobs:
  eslint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx eslint --max-warnings=0 .
```

`--max-warnings=0` makes warnings fail the build. The trade-off:
either fix the warning or change the rule's severity in the
config. The middle ground (warning forever, never triaged) is
the failure mode this prevents.

## When the team disagrees about a rule

1. Is it _logical_, _stylistic_, or _formatting_?
2. If _formatting_ — move to Prettier; the question is settled.
3. If _stylistic_ — pick one, document the choice in a comment
   in `eslint.config.js`, stop arguing.
4. If _logical_ — read the rule's documented examples. Decide
   per the _intent_ of the rule, not per the most recent PR
   that triggered it.

The classification step typically resolves the disagreement before reaching steps 3 or 4.

## Referenced by

- [Dead-code detection](https://quality.stereobooster.com/dead-code-detection.md) · Methods
- [Linters](https://quality.stereobooster.com/linters.md) · Methods
- [Verifying time and date handling](https://quality.stereobooster.com/time-and-date.md) · Methods
- [Recipes](https://quality.stereobooster.com/recipes.md) · Recipes
- [TypeScript](https://quality.stereobooster.com/typescript.md) · Recipes

## References

[^typescripteslintnd]: typescript-eslint maintainers. n.d. *[What About Formatting?](https://typescript-eslint.io/users/what-about-formatting/)* Typescript-eslint Documentation. <https://typescript-eslint.io/users/what-about-formatting/>.
