cheat sheet

ESLint

The standard JavaScript/TypeScript linter. Covers flat config (v9), legacy .eslintrc, running, rules, plugins, TypeScript integration, VS Code, and pre-commit hooks.

ESLint

What it is

ESLint is the standard JavaScript and TypeScript linter. It finds bugs, enforces code style, and applies configurable rules via a plugin ecosystem. ESLint v9 (released April 2024) switched to flat config (eslint.config.js) by default; v8 uses the legacy .eslintrc.* format.

Install

bash
# npm
npm install -D eslint

# Create a config interactively (recommended for new projects)
npm init @eslint/config@latest

Output:

text
✔ How would you like to use ESLint? · problems
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · none
✔ Does your project use TypeScript? · yes
✔ Where does your code run? · node
The config that you've selected requires the following dependencies:
eslint, @eslint/js, typescript-eslint
✔ Would you like to install them now? · Yes

Flat config — eslint.config.js (v9, default)

Minimal JavaScript config

javascript
// eslint.config.js
import js from "@eslint/js";

export default [
  js.configs.recommended,
  {
    rules: {
      "no-unused-vars": "warn",
      "no-console": "warn",
      eqeqeq: "error",
    },
  },
];

Full TypeScript config

javascript
// eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import globals from "globals";

export default tseslint.config(
  // Apply ESLint recommended rules to all JS/TS files
  js.configs.recommended,

  // Apply TypeScript-ESLint recommended rules to .ts/.tsx files
  ...tseslint.configs.recommended,

  {
    languageOptions: {
      globals: {
        ...globals.node,
        ...globals.browser,
      },
    },
    rules: {
      "@typescript-eslint/no-explicit-any": "warn",
      "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
      "no-console": ["warn", { allow: ["warn", "error"] }],
      eqeqeq: "error",
      curly: "error",
    },
  },

  // Ignore generated files and dependencies
  {
    ignores: ["dist/**", "build/**", "node_modules/**", "*.min.js"],
  }
);

React + TypeScript config

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

export default tseslint.config(
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    plugins: {
      react: reactPlugin,
      "react-hooks": reactHooks,
    },
    languageOptions: {
      globals: globals.browser,
      parserOptions: { ecmaFeatures: { jsx: true } },
    },
    settings: { react: { version: "detect" } },
    rules: {
      ...reactPlugin.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      "react/react-in-jsx-scope": "off", // Not needed in React 17+
    },
  },
  { ignores: ["dist/**", "node_modules/**"] }
);

Legacy config — .eslintrc.* (v8)

json
// .eslintrc.json
{
  "env": { "node": true, "es2022": true },
  "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "rules": {
    "no-unused-vars": "off",
    "@typescript-eslint/no-unused-vars": "error",
    "eqeqeq": "error"
  },
  "ignorePatterns": ["dist/", "node_modules/"]
}

If you are on v8 but want to migrate to v9 flat config, run npx @eslint/migrate-config .eslintrc.json to get a generated eslint.config.js as a starting point.

Running ESLint

bash
# Lint all files
npx eslint .

# Lint a specific directory
npx eslint src/

# Lint and auto-fix fixable issues
npx eslint src/ --fix

# Fail if there are any warnings (useful in CI)
npx eslint . --max-warnings 0

# Output as JSON (for tooling)
npx eslint . --format json > eslint-report.json

# Lint specific file types
npx eslint "src/**/*.{js,ts,jsx,tsx}"

# Print the resolved config for a file (debug)
npx eslint --print-config src/index.ts

Output (typical lint run):

text
/home/user/project/src/app.ts
  12:5  warning  Unexpected console statement  no-console
  34:3  error    Missing semicolon             semi

✖ 2 problems (1 error, 1 warning)
  0 errors and 1 warning potentially fixable with the `--fix` option.

Rule severity levels

javascript
// In the rules object:
"rule-name": 0          // off
"rule-name": 1          // warn
"rule-name": 2          // error

// String equivalents (preferred for readability):
"rule-name": "off"
"rule-name": "warn"
"rule-name": "error"

// With options — use an array:
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
"no-console": ["warn", { "allow": ["warn", "error"] }]

Key plugins

bash
# TypeScript
npm install -D typescript-eslint

# React
npm install -D eslint-plugin-react eslint-plugin-react-hooks

# Import order
npm install -D eslint-plugin-import

# Node.js best practices
npm install -D eslint-plugin-n

# Accessibility (jsx-a11y)
npm install -D eslint-plugin-jsx-a11y

# Unicorn (extra opinionated rules)
npm install -D eslint-plugin-unicorn

Output: (none — exits 0 on success)

eslint-plugin-import example

javascript
// eslint.config.js (flat config)
import importPlugin from "eslint-plugin-import";

export default [
  {
    plugins: { import: importPlugin },
    rules: {
      "import/no-duplicates": "error",
      "import/order": [
        "warn",
        {
          groups: ["builtin", "external", "internal", "parent", "sibling"],
          "newlines-between": "always",
          alphabetize: { order: "asc" },
        },
      ],
    },
  },
];

Inline rule disabling

javascript
// Disable next line
// eslint-disable-next-line no-console
console.log("debug");

// Disable current line
doSomething(); // eslint-disable-line no-alert

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

// Disable entire file (put at top)
/* eslint-disable */

VS Code integration

Install the ESLint extension (dbaeumer.vscode-eslint), then add to .vscode/settings.json:

json
{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },
  "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
  "eslint.useFlatConfig": true
}

Pre-commit hooks with lint-staged

bash
npm install -D husky lint-staged
npx husky init

Output: (none — exits 0 on success)

Add to package.json:

json
{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix --max-warnings 0",
      "prettier --write"
    ]
  }
}

Add to .husky/pre-commit:

bash
npx lint-staged

Output: (none — exits 0 on success)

package.json scripts

json
{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "lint:ci": "eslint . --max-warnings 0"
  }
}

CI usage (GitHub Actions)

yaml
- name: Lint
  run: npx eslint . --max-warnings 0

In CI always use --max-warnings 0 so that warnings become pipeline failures. In local development, warnings are fine as soft guidance.

Flat config — deep dive

Flat config replaces the legacy .eslintrc cascade with a single array of configuration objects. Each object can apply to all files, a subset (via files), or be a pure ignore record. ESLint evaluates the array top-to-bottom and merges objects whose files glob matches the file being linted — the last writer wins for rules, languageOptions, and plugins. This makes overrides explicit (no extends-graph chasing) and removes the need for .eslintignore — ignores live inside the config.

Anatomy of a flat config object

javascript
// eslint.config.js
export default [
  {
    name: "my-rules",                      // optional label (shows in --print-config)
    files: ["**/*.{ts,tsx}"],              // glob filter (default: all files ESLint sees)
    ignores: ["**/*.test.ts"],             // local ignore (applies only to this block)
    languageOptions: {
      ecmaVersion: 2024,                   // syntax version to parse
      sourceType: "module",                // "module" | "commonjs" | "script"
      parser: undefined,                   // override default espree parser
      parserOptions: { project: true },    // parser-specific options
      globals: { window: "readonly" },     // declared globals (no-undef)
    },
    linterOptions: {
      reportUnusedDisableDirectives: "warn", // flag stale /* eslint-disable */
      noInlineConfig: false,                 // forbid inline overrides
    },
    plugins: { unicorn: unicornPlugin },   // plugin namespace → object
    rules: {
      "unicorn/no-null": "warn",
    },
    settings: { react: { version: "detect" } }, // shared between plugins
  },
];

Ignore-only objects

A config object containing only an ignores key applies globally — equivalent to the legacy .eslintignore. Mix this with per-block ignores for surgical exclusions:

javascript
export default [
  // Global ignores — never lint these
  { ignores: ["dist/**", "coverage/**", "**/*.min.js", "**/__generated__/**"] },

  // Block-local ignore — applies to following rules block only
  {
    files: ["**/*.ts"],
    ignores: ["**/*.d.ts"],
    rules: { "@typescript-eslint/no-unused-vars": "error" },
  },
];

Composing shared configs

Tools like typescript-eslint and @eslint/js expose pre-built config arrays. Spread them into your own array, then layer overrides afterwards:

javascript
import js from "@eslint/js";
import tseslint from "typescript-eslint";

export default [
  js.configs.recommended,
  ...tseslint.configs.strictTypeChecked,   // strict + type-aware rules
  ...tseslint.configs.stylisticTypeChecked,
  {
    languageOptions: { parserOptions: { project: "./tsconfig.json" } },
    rules: {
      "@typescript-eslint/consistent-type-imports": "error",
      "@typescript-eslint/no-misused-promises": "error",
    },
  },
];

tseslint.config(...) is a helper that performs the same array-flatten with TypeScript-friendly types — prefer it when you want IntelliSense on rule names.

Type-aware linting

Some @typescript-eslint rules (e.g. no-floating-promises, no-misused-promises, await-thenable) need access to the TypeScript type checker. Enable them by pointing the parser at tsconfig.json:

javascript
import tseslint from "typescript-eslint";

export default tseslint.config({
  files: ["**/*.{ts,tsx}"],
  languageOptions: {
    parserOptions: {
      project: ["./tsconfig.json"],   // or projectService: true for v8+
      tsconfigRootDir: import.meta.dirname,
    },
  },
  extends: [...tseslint.configs.recommendedTypeChecked],
});

Type-aware linting is 5–10× slower than syntactic linting because each file requires program-wide type inference. Restrict the files pattern to source code, never include dist/, and consider running it only in CI if dev-loop latency matters.

Plugin ecosystem deep dive

Plugins extend ESLint with new rules, configs, processors, and parsers. They register themselves under a namespace inside plugins and expose rules under that namespace (e.g. react/no-unused-prop-types).

typescript-eslint

The canonical TypeScript plugin (a meta-package re-exporting @typescript-eslint/eslint-plugin and @typescript-eslint/parser). Provides the parser that understands TypeScript syntax and ~150 rules covering types, async, naming, unused code, and stylistic concerns. Three preset tiers: recommended (safe defaults), strict (more opinionated), and *TypeChecked variants that require the type-aware setup above.

javascript
import tseslint from "typescript-eslint";

export default tseslint.config(
  ...tseslint.configs.strict,
  {
    rules: {
      "@typescript-eslint/no-explicit-any": "error",
      "@typescript-eslint/consistent-type-definitions": ["error", "type"],
      "@typescript-eslint/array-type": ["error", { default: "array-simple" }],
      "@typescript-eslint/no-non-null-assertion": "error",
    },
  },
);

eslint-plugin-react and eslint-plugin-react-hooks

React-specific lints: JSX accessibility scaffolding, prop-types vs TS, hooks rules-of-hooks enforcement, and exhaustive-deps for useEffect/useMemo/useCallback. The hooks plugin is mandatory for any React codebase — its react-hooks/exhaustive-deps rule catches the most common React bug (stale closures over state).

javascript
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import jsxA11y from "eslint-plugin-jsx-a11y";

export default [
  {
    files: ["**/*.{jsx,tsx}"],
    plugins: {
      react,
      "react-hooks": reactHooks,
      "jsx-a11y": jsxA11y,
    },
    settings: { react: { version: "detect" } },
    rules: {
      ...react.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      ...jsxA11y.configs.recommended.rules,
      "react/react-in-jsx-scope": "off",
      "react/prop-types": "off",            // using TypeScript instead
      "react-hooks/exhaustive-deps": "error",
    },
  },
];

eslint-plugin-import (and eslint-plugin-import-x)

Lints ES module import/export syntax: detects circular dependencies, missing files, unresolved paths, duplicate imports, and enforces import ordering. eslint-plugin-import-x is a faster fork actively maintained for flat config.

javascript
import importX from "eslint-plugin-import-x";

export default [
  importX.flatConfigs.recommended,
  importX.flatConfigs.typescript,
  {
    rules: {
      "import-x/no-cycle": ["error", { maxDepth: 5 }],
      "import-x/no-unresolved": "error",
      "import-x/order": [
        "warn",
        {
          groups: [
            "builtin",
            "external",
            "internal",
            ["parent", "sibling", "index"],
            "type",
          ],
          "newlines-between": "always",
          alphabetize: { order: "asc", caseInsensitive: true },
        },
      ],
    },
  },
];

eslint-plugin-security

Static analysis for common Node.js security pitfalls: unsafe regex (ReDoS), eval(), child_process with user-controlled args, path traversal via fs calls, and pseudo-random number generators where crypto is needed. False-positive rate is moderate — review findings rather than auto-fixing.

javascript
import security from "eslint-plugin-security";

export default [
  security.configs.recommended,
  {
    rules: {
      "security/detect-object-injection": "off",  // very noisy in real code
    },
  },
];

eslint-plugin-n (Node.js)

Successor to eslint-plugin-node. Enforces Node-specific best practices: importing only declared dependencies, preferring node: protocol imports (import fs from "node:fs"), no use of deprecated APIs, and no use of features above the version declared in engines.node.

eslint-plugin-unicorn

Opinionated quality rules: prefer Array.from, prefer String.replaceAll, prefer Number.parseInt, prefer top-level await, no process.exit(), prefer node: imports, consistent file naming. Highly stylistic — pick the rules you agree with rather than enabling recommended wholesale.

Shareable configs

A shareable config is just an npm package that exports a flat config array. Convention: name them eslint-config-<n> (then import as <n>) or @scope/eslint-config-<n>. Popular examples:

ConfigDescription
eslint-config-airbnbAirbnb's opinionated rules (legacy .eslintrc only; v9 fork: eslint-config-airbnb-extended)
eslint-config-standardStandard JS style
eslint-config-xoStrict but practical
@antfu/eslint-configAnthony Fu's flat-config-native preset, very modern
@vercel/style-guideVercel's preset for Next.js projects
javascript
// Using @antfu/eslint-config
import antfu from "@antfu/eslint-config";

export default antfu({
  typescript: true,
  vue: false,
  react: true,
  stylistic: { indent: 2, quotes: "double", semi: true },
});

Autofix — --fix mechanics and safety

eslint --fix rewrites source files in place by applying each rule's fix function. Not every rule is auto-fixable; those that are usually annotate themselves with the wrench icon in their documentation. Autofixes are categorized by ESLint:

TypeBehaviourFlag
Safe fixesMechanically correct, no semantics change--fix
Suggestion fixesProbably correct, may change behaviour--fix-type suggestion
Layout fixesWhitespace/style only--fix-type layout
Problem fixesCode may have been buggy--fix-type problem
bash
# Only apply layout fixes (no semantic edits)
npx eslint . --fix --fix-type layout

# Show what would be fixed without writing
npx eslint . --fix-dry-run --format json

# Fix only one rule
npx eslint . --fix --rule '{"semi": "error"}' --no-eslintrc

Output (dry run):

text
[{"filePath":"/proj/src/app.ts","messages":[],"output":"const x = 1;\n","fixableErrorCount":1,"fixableWarningCount":0}]

Always commit (or stash) before running --fix. The autofixer for some rules (e.g. no-unused-vars autofix from third-party plugins) can delete code. Review the diff before staging.

Conflicting autofixes

When two rules want to fix the same range, ESLint applies fixes in passes until the source stabilises (max 10 passes by default). Watch for oscillation: rule A rewrites to X, rule B rewrites back to Y, repeat. ESLint detects this and reports Unfixable due to conflicting rules. Common offenders are stylistic rules that conflict with Prettier — which is exactly why eslint-config-prettier exists.

ESLint vs Biome — when to use which

Biome (biome) is a Rust-based linter + formatter aimed at replacing ESLint and Prettier with a single binary. It's 10–100× faster, has zero plugin runtime, and ships rules compiled into the binary. The trade-off is plugin maturity.

ConcernESLint (v9 flat config)Biome
LanguagesJS, TS, JSX, TSX (via plugins: Vue, Svelte, MD, YAML)JS, TS, JSX, TSX, JSON, CSS, GraphQL
Formatter includedNo — pair with PrettierYes
Plugin ecosystemHundreds of plugins, 15+ years of rulesLimited (WASM plugins in v2, early)
Cold-start speed (1k files)5–15 s0.1–0.5 s
Config fileseslint.config.js + .prettierrcbiome.json
Type-aware lintingYes (typescript-eslint)No (parses syntax only)
React Hooks rulesYes (eslint-plugin-react-hooks)Partial (useExhaustiveDependencies)
Editor supportMature — VS Code, IntelliJ, VimGood — VS Code, Zed, Neovim

Choose ESLint when: you need type-aware rules, depend on a niche plugin (React-Query lints, Tailwind, GraphQL Schema), or have a deeply-customised rule set. Choose Biome when: you want one tool with one config, value speed, and your rule needs fit the recommended ruleset. Hybrid is also legitimate — run Biome for format + import sort, ESLint for type-aware rules only — but pay attention to the slowdown that re-enables.

Common pitfalls

  • "Cannot find module 'eslint-config-prettier'" after switching to flat config — extends: strings work only in legacy .eslintrc. In flat config, import the config and spread/concat it into the array.
  • parserOptions.project makes lint 10× slower — type-aware rules require tsc-level type inference for every file. Scope to source only, or use projectService: true (typescript-eslint v8+) which is lazier.
  • Flat config + IDE: nothing lints — older VS Code ESLint extensions need "eslint.useFlatConfig": true (now the default in recent versions, but still pinned in some settings).
  • Glob doesn't match .tsxfiles: ["**/*.ts"] does NOT match .tsx. Use files: ["**/*.{ts,tsx}"] or ["**/*.ts", "**/*.tsx"].
  • --fix modifies generated files — make sure dist/, build/, .next/, and coverage/ are in a global ignores block.
  • no-unused-vars flags React imports — set varsIgnorePattern: "^React$" or use the new JSX transform (react/react-in-jsx-scope: off).
  • Mixed .eslintrc + eslint.config.js — ESLint v9 prefers flat config and ignores .eslintrc.* by default. Delete the legacy file once migrated to avoid confusion.
  • overrides is gone in flat config — what used to be overrides: [{ files, rules }] is now a separate top-level object in the array. Don't translate the legacy shape literally.
  • extends is gone in flat config — spread the config array instead: [...sharedConfig, { rules: { ... } }].

Real-world recipes

Lint only changed files in a pre-commit hook

lint-staged already does this for git-staged paths. For a manual run against main:

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

Output:

text
src/components/Card.tsx
  12:3  warning  Unexpected console statement  no-console

✖ 1 problem (0 errors, 1 warning)

Lint a monorepo with workspace-specific overrides

javascript
// eslint.config.js at the repo root
import baseConfig from "./eslint.base.js";
import reactConfig from "./eslint.react.js";

export default [
  ...baseConfig,
  {
    files: ["apps/web/**/*.{ts,tsx}"],
    ...reactConfig[0],
  },
  {
    files: ["packages/server/**/*.ts"],
    languageOptions: { globals: { ...globals.node } },
  },
];

Cache lint results for fast re-runs

bash
# Cache lint results — only re-lint changed files
npx eslint . --cache --cache-location node_modules/.cache/eslint/

# Invalidate cache (after upgrading a plugin)
rm -rf node_modules/.cache/eslint/

Output: (first run lints everything; second run is near-instant)

bash
npx eslint --print-config src/index.ts | jq '.rules | keys'

Output:

text
[
  "@typescript-eslint/consistent-type-imports",
  "@typescript-eslint/no-explicit-any",
  "no-console",
  "eqeqeq",
  ...
]

Generate a Markdown report from JSON output

bash
npx eslint . --format json \
  | jq -r '.[] | select(.errorCount + .warningCount > 0) | "- `\(.filePath | sub(".*/"; ""))` — \(.errorCount) errors, \(.warningCount) warnings"' \
  > lint-report.md

Output (lint-report.md):

text
- `app.ts` — 1 errors, 0 warnings
- `helpers.ts` — 0 errors, 3 warnings

Disable a rule for one folder only

javascript
export default [
  ...baseConfig,
  {
    files: ["scripts/**/*.{js,ts}"],
    rules: {
      "no-console": "off",       // scripts/ may log freely
      "no-process-exit": "off",
    },
  },
];

See also

  • Prettier — pair with ESLint via eslint-config-prettier
  • Biome — Rust-based all-in-one alternative
  • Vite — most modern bundler with first-class ESLint integration
  • Vitest — pairs with eslint-plugin-vitest for test-file lints
  • TypeScript installation — required for typescript-eslint