cheat sheet

eslint

Package-level reference for eslint on npm — install variants, flat config (v9) vs legacy .eslintrc, typescript-eslint, plugin ecosystem, and alternatives.

eslint

What it is

eslint is the long-standing pluggable static-analysis tool for JavaScript and TypeScript, created by Nicholas C. Zakas in 2013. It parses code into an AST (default espree; TypeScript uses @typescript-eslint/parser) and runs configured rules across the tree, emitting warnings and errors. Rules ship in core, in framework plugins (eslint-plugin-react, eslint-plugin-vue), and in language plugins (typescript-eslint).

The configuration model split in v9: flat config (eslint.config.js — single file, explicit array of config objects) is now mandatory, replacing the legacy hierarchical .eslintrc.* files that walked the directory tree merging configs.

Install

bash
# Always a devDep
npm install -D eslint
pnpm add -D eslint
yarn add -D eslint
bun add -d eslint

Output: eslint binary on PATH; expects an eslint.config.js at repo root.

bash
# Scaffold a flat config interactively (v9+)
npm init @eslint/config@latest

Output: writes eslint.config.js (or .mjs / .cjs) plus any deps it selects (e.g. typescript-eslint, eslint-plugin-react).

bash
# Day-to-day
npx eslint .                        # lint everything
npx eslint . --fix                  # apply auto-fixes
npx eslint src/foo.ts --max-warnings 0   # strict mode

Output: per-file diagnostics with <line>:<col> <severity> <message> <rule-id>; exits non-zero on any error.

Versioning & Node support

  • Current major line is 9.x (released April 2024). 8.x is now in extended LTS only (security fixes); 7.x is EOL.
  • v9 requires Node 18.18+, 20.9+, or 21.1+ and dropped support for Node 16.
  • Flat config (eslint.config.js) is mandatory in v9 — legacy .eslintrc.* files are ignored. Migration shim @eslint/eslintrc lets you compat over old configs during transition.
  • eslint ships as CJS but understands ESM configs. Always a dev dependency.
  • Loose semver — major bumps typically remove rules and config formats.

Package metadata

  • Maintainer: OpenJS Foundation / ESLint TSC (Nicholas C. Zakas + core team)
  • Project home: github.com/eslint/eslint
  • Docs: eslint.org/docs
  • npm: npmjs.com/package/eslint
  • License: MIT
  • First released: 2013
  • Downloads: tens of millions per week — top-5 dev tool on npm.

Peer dependencies & extras

ESLint core has minimal peers; all the surface comes from plugins:

PackagePurpose
typescript-eslint (single package, v8+)TypeScript parser + recommended rule sets. Replaced the older @typescript-eslint/parser + @typescript-eslint/eslint-plugin pair for flat config; both still exist for legacy configs.
@eslint/jsOfficial core JS rule presets exposed as flat-config objects (js.configs.recommended).
eslint-plugin-react / eslint-plugin-react-hooksReact + hooks rules. Hooks plugin is essentially mandatory for React projects.
eslint-plugin-vueVue 2/3 rules.
eslint-plugin-svelteSvelte rules.
eslint-plugin-astro.astro support via astro-eslint-parser.
eslint-plugin-import / eslint-plugin-import-xImport-graph validation. import-x is the maintained fork most flat-config projects use.
eslint-plugin-unicornOpinionated extras (regex safety, modern API preference, etc.).
eslint-plugin-jsx-a11yAccessibility rules for JSX.
eslint-plugin-nNode-specific rules (successor to eslint-plugin-node).
eslint-config-prettierDisables stylistic rules that conflict with Prettier. Almost always used alongside Prettier.
eslint-plugin-prettierRuns Prettier as an ESLint rule (slower; prefer eslint-config-prettier).

Alternatives

ToolTrade-off
@biomejs/biomeRust, single binary, linter + formatter, 25–35× faster. Coverage ~95% of common ESLint rules; missing some plugins. Strong pick for new projects.
oxlint (part of oxc)Rust, ESLint-compatible, very fast, intended to run alongside ESLint for the rules it supports. Younger, narrower rule coverage.
typescript-eslintA layer on top of ESLint, not an alternative — gives TS parsing + type-aware rules. Always paired with ESLint, never replacing it.
deno lintBundled with Deno. Tied to the Deno ecosystem.
standard / standardx / xoWrapped, opinionated ESLint configs. Replace your config, not ESLint.

Common gotchas

  1. v9 flat config is mandatory. Many plugins lagged the migration — projects that bumped to v9 hit "Cannot use this plugin with flat config" errors for months. Use @eslint/compat's fixupPluginRules() to wrap legacy plugins until they ship native flat-config exports.
  2. --fix is destructive for some rules. Rules like no-unused-vars (with argsIgnorePattern) or prefer-const are safe; rules with auto-fixers that rewrite logic (e.g. unicorn/prefer-spread) can change semantics on edge cases. Review git diff after every --fix run on production code.
  3. Type-aware rules are slow. typescript-eslint's recommended-type-checked preset parses the TS program — adds seconds-to-minutes to lint runs on large codebases. Configure languageOptions.parserOptions.project and consider scoping type-aware rules to a separate eslint.config.js entry.
  4. Plugin resolution paths differ across managers. pnpm's strict layout sometimes hides plugins. Flat config resolves plugins from the config file directly (via import), which sidesteps this — another reason to migrate off .eslintrc.
  5. Disabling a rule inline is a 3-form trap. // eslint-disable-next-line, /* eslint-disable */, and /* eslint-disable-next-line */ (block-comment, next-line variant) behave differently. The first two are the safe choices; mix them up and you'll silently disable far more than you intended.
  6. ignores in flat config replaces .eslintignore. v9 deprecated .eslintignore. Add { ignores: ["dist/**", ".astro/**"] } as the first entry of the flat config array — a standalone object with only ignores becomes a "global ignore".
  7. Performance: rule cost varies wildly. Run eslint --debug or TIMING=1 eslint . to surface which rules are slow. import/no-cycle and any type-aware rule routinely dominate runtime; consider running them in CI only.

Real-world recipes

The patterns that come up when you're actually wiring ESLint into a fresh repo.

Flat config from scratch (TS + React + Prettier)

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

export default [
  { ignores: ["dist/**", "build/**", ".next/**", "coverage/**"] },
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    files: ["**/*.{ts,tsx}"],
    languageOptions: {
      parserOptions: {
        project: ["./tsconfig.json"],
        tsconfigRootDir: import.meta.dirname,
      },
    },
    plugins: { react, "react-hooks": reactHooks },
    rules: {
      ...react.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      "react/react-in-jsx-scope": "off",
    },
    settings: { react: { version: "detect" } },
  },
  prettier,
];

prettier must be last so it overrides any stylistic rules from earlier configs.

Monorepo flat config

A single root config that varies per package:

javascript
// eslint.config.js
import baseConfig from "./eslint.config.base.js";

export default [
  ...baseConfig,
  {
    files: ["packages/api/**/*.ts"],
    rules: { "no-console": ["error", { allow: ["error"] }] },
  },
  {
    files: ["packages/web/**/*.{ts,tsx}"],
    languageOptions: { globals: { window: "readonly", document: "readonly" } },
  },
];

Each files block scopes by path; configs are merged in order (last wins).

Custom rule — ban an internal import

javascript
// eslint-custom-rules/no-internal-imports.js
export default {
  meta: { type: "problem", schema: [], messages: { internalImport: "Do not import from internal paths." } },
  create(context) {
    return {
      ImportDeclaration(node) {
        if (node.source.value.includes("/internal/")) {
          context.report({ node, messageId: "internalImport" });
        }
      },
    };
  },
};
javascript
// eslint.config.js
import customRules from "./eslint-custom-rules/index.js";

export default [
  {
    plugins: { custom: { rules: customRules } },
    rules: { "custom/no-internal-imports": "error" },
  },
];

Per-rule severity per file

javascript
{
  files: ["scripts/**/*.ts", "tools/**/*.ts"],
  rules: {
    "no-console": "off",                  // scripts can console.log
    "@typescript-eslint/no-explicit-any": "off",
  },
}

Disable a rule for one line

typescript
// eslint-disable-next-line no-console
console.log("intentional");

const x = 1; // eslint-disable-line @typescript-eslint/no-unused-vars

Block-disable for a region:

typescript
/* eslint-disable no-console */
console.log("a");
console.log("b");
/* eslint-enable no-console */

Production deployment

ESLint is dev-time only; "deployment" is CI integration.

CI as a gate

yaml
# .github/workflows/lint.yml
- run: npx eslint . --max-warnings=0

--max-warnings=0 makes any warning a CI failure — useful for staged rollout where new rules start as warnings before promotion to errors.

Cache for fast incremental runs

bash
npx eslint . --cache --cache-location=.eslintcache

Output:

text
✔ No ESLint warnings or errors  (cache: 287/312 files unchanged, 25 re-linted)

.eslintcache is git-ignored; CI can actions/cache@v4 it for cross-run reuse.

Per-PR scope

For huge repos, lint only changed files:

bash
git diff --name-only --diff-filter=ACMR origin/main | grep -E '\.(ts|tsx|js|jsx)$' | xargs npx eslint --max-warnings=0

Output:

text
src/components/UserCard.tsx
  17:9  warning  'user' is defined but never used  @typescript-eslint/no-unused-vars

✖ 1 problem (0 errors, 1 warning)

Tradeoff: a config change won't re-validate untouched files. Run the full lint nightly as a backstop.

eslint --fix is not for CI

--fix rewrites files. Never run in CI on someone else's commit — it's a code-injection vector on pull_request_target workflows. Reserve for local + pre-commit.

Performance tuning

ESLint v9 is faster than v8, but type-aware rules dominate runtime on large codebases.

TIMING=1

bash
TIMING=1 npx eslint . | head -30

Output:

text
Rule                                          | Time (ms) | Relative
:---------------------------------------------|----------:|--------:
@typescript-eslint/no-floating-promises       |  1842.301 |    34.2%
@typescript-eslint/no-misused-promises        |  1104.118 |    20.5%
import/no-cycle                               |   712.504 |    13.2%
@typescript-eslint/no-unsafe-assignment       |   388.221 |     7.2%

Lists rules sorted by total time. The usual suspects: import/no-cycle, @typescript-eslint/no-floating-promises, @typescript-eslint/no-misused-promises — any rule that walks the type graph.

Scope type-aware rules

Use a separate config for type-aware rules and run it only in CI:

javascript
// eslint.config.js (default — fast)
export default [js.configs.recommended];

// eslint.config.ci.js (slow — type-aware)
export default [
  ...tseslint.configs.recommendedTypeChecked,
  { languageOptions: { parserOptions: { project: "./tsconfig.json" } } },
];
json
{ "scripts": { "lint": "eslint .", "lint:ci": "eslint --config eslint.config.ci.js ." } }

Parallel via eslint-d or Nx

eslint_d runs ESLint as a long-lived daemon — re-uses parser state across runs, 5-10× faster for repeated small lints (editor on-save).

bash
npm install -g eslint_d
eslint_d .

Output:

text
added 1 package in 4s
✔ No ESLint warnings or errors  (daemon: 18ms)

Nx and Turborepo offer per-package caching that's more sophisticated than --cache.

ESM/CJS interop & bundling

ESLint core is CJS internally but understands ESM configs. The eslint.config.js file:

  • .js — interpreted per package.json "type" field
  • .mjs — always ESM
  • .cjs — always CJS

Plugins can publish ESM, CJS, or both. Flat config imports plugins directly:

javascript
import react from "eslint-plugin-react";   // ESM
const react = require("eslint-plugin-react"); // CJS — only in .cjs config

The legacy .eslintrc resolved plugins by name string; flat config resolves by import, which is more robust (no plugin-discovery issues in pnpm).

Version migration guide

From → ToHighlights
7 → 8Node 12+. eslint:recommended rules updated. Plugin authors got a new meta.fixable requirement.
8 → 9Flat config mandatory. Legacy .eslintrc.* files ignored unless ESLINT_USE_FLAT_CONFIG=false. Node 18.18+ required. Some core rules removed (formatters moved to @stylistic). Many plugins lagged 6-12 months.
typescript-eslint 7 → 8Single package (typescript-eslint) replaces @typescript-eslint/parser + @typescript-eslint/eslint-plugin pair for flat config. Legacy two-package form still works for old configs.

Common 8→9 friction

  • Plugins not yet flat-config-native — wrap with @eslint/compat's fixupPluginRules():
    javascript
    import { fixupPluginRules } from "@eslint/compat";
    import legacyPlugin from "eslint-plugin-legacy";
    
    export default [{ plugins: { legacy: fixupPluginRules(legacyPlugin) } }];
    
  • .eslintignore removed — move to a top-level { ignores: [...] } config block.
  • extends removed — flat config uses spread (...preset) instead.
  • env, globals, parser moved — all under languageOptions.

Security considerations

  1. Plugin install = arbitrary code. ESLint plugins run with full Node permissions. Audit; pin exact; review changelogs.
  2. eslint-plugin-security flags common Node security issues (detect-eval-with-expression, detect-non-literal-require). Enable on Node service repos.
  3. no-eval / no-implied-eval — core rules; always on.
  4. no-restricted-imports can block dangerous packages or modules with security implications:
    javascript
    "no-restricted-imports": ["error", { patterns: ["lodash/*"], paths: [{ name: "fs", importNames: ["readFileSync"], message: "Use fs/promises" }] }]
    
  5. CI execution of --fix is a code-injection vector on pull_request_target. Only run on pull_request (read-only checkout).

Testing strategies

ESLint itself is a testing strategy — every CI run is a static-analysis test. Beyond that:

  • eslint --rule '{"my-rule": "error"}' to test a custom rule on the CLI.
  • @typescript-eslint/rule-tester for custom rule unit tests — provides typed test harness.
  • vitest integration — run npx eslint . from a Vitest test to fail CI on lint errors via the test suite.

Configuration patterns

Per-environment globals

javascript
{
  files: ["src/server/**/*.ts"],
  languageOptions: { globals: { ...globals.node } },
},
{
  files: ["src/client/**/*.ts"],
  languageOptions: { globals: { ...globals.browser } },
},

Override extra strict for one path

javascript
{
  files: ["src/critical/**/*.ts"],
  rules: {
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/strict-boolean-expressions": "error",
    "no-restricted-syntax": ["error", "ThrowStatement"],
  },
}

Project references with parserOptions.project

javascript
{
  languageOptions: {
    parserOptions: {
      project: ["./tsconfig.eslint.json"],
      tsconfigRootDir: import.meta.dirname,
    },
  },
}

Use a dedicated tsconfig.eslint.json that extends the main one but "include"s test files and configs — keeps type-aware rules running across everything.

Troubleshooting common errors

  • Invalid Options: 'extends' — flat config doesn't accept extends. Use spread: [...js.configs.recommended, { rules: { ... } }].
  • Plugin "foo" was conflicting with another plugin — two configs registered the same plugin key. Use a unique key per plugin instance.
  • Parsing error: '>' expected — TypeScript file isn't picked up by TS parser. Set files: ["**/*.{ts,tsx}"] on the TS-specific config block.
  • 'X' is not defined (no-undef) in TS code — disable no-undef for TS files; TypeScript already enforces. The recommended TS configs do this by default.
  • ENOENT: no such file or directory for .eslintignore.eslintignore is removed in v9. Use ignores in flat config.

Ecosystem integrations

The most-used plugins beyond the install table:

PluginWhat it covers
eslint-plugin-react / eslint-plugin-react-hooksReact + Hooks rules (mandatory for React)
eslint-plugin-vueVue 2/3
eslint-plugin-svelteSvelte
eslint-plugin-astroAstro
eslint-plugin-import-xImport-graph validation (maintained fork of eslint-plugin-import)
eslint-plugin-unicornOpinionated modern-JS rules
eslint-plugin-jsx-a11yAccessibility
eslint-plugin-jest / eslint-plugin-vitestTest-runner-aware rules
eslint-plugin-tailwindcssClass-order, conflicting-classes checks
eslint-plugin-securityNode security
eslint-plugin-nNode-specific (successor to eslint-plugin-node)
eslint-config-prettierDisables conflicting stylistic rules

When NOT to use this

  • New small projects. Biome covers ~95% of common ESLint rules in one binary, 25-35× faster. Pick for new projects without a specific plugin need.
  • Pure JS-only tooling. Stylelint covers CSS better; markdownlint covers Markdown better. ESLint's strength is JS/TS.
  • Editor performance issues. ESLint's editor integration parses on every keystroke for some rules. If editor lag is intolerable, switch to eslint_d or limit ESLint to type-fast rules in the editor and run the slow ones in CI.

See also