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
# npm
npm install -D eslint
# Create a config interactively (recommended for new projects)
npm init @eslint/config@latest
Output:
✔ 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
// 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
// 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
// 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)
// .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.jsonto get a generatedeslint.config.jsas a starting point.
Running ESLint
# 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):
/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
// 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
# 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
// 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
// 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:
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
"eslint.useFlatConfig": true
}
Pre-commit hooks with lint-staged
npm install -D husky lint-staged
npx husky init
Output: (none — exits 0 on success)
Add to package.json:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix --max-warnings 0",
"prettier --write"
]
}
}
Add to .husky/pre-commit:
npx lint-staged
Output: (none — exits 0 on success)
package.json scripts
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"lint:ci": "eslint . --max-warnings 0"
}
}
CI usage (GitHub Actions)
- name: Lint
run: npx eslint . --max-warnings 0
In CI always use
--max-warnings 0so 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
// 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:
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:
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:
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.
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).
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.
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.
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:
| Config | Description |
|---|---|
eslint-config-airbnb | Airbnb's opinionated rules (legacy .eslintrc only; v9 fork: eslint-config-airbnb-extended) |
eslint-config-standard | Standard JS style |
eslint-config-xo | Strict but practical |
@antfu/eslint-config | Anthony Fu's flat-config-native preset, very modern |
@vercel/style-guide | Vercel's preset for Next.js projects |
// 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:
| Type | Behaviour | Flag |
|---|---|---|
| Safe fixes | Mechanically correct, no semantics change | --fix |
| Suggestion fixes | Probably correct, may change behaviour | --fix-type suggestion |
| Layout fixes | Whitespace/style only | --fix-type layout |
| Problem fixes | Code may have been buggy | --fix-type problem |
# 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):
[{"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-varsautofix 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.
| Concern | ESLint (v9 flat config) | Biome |
|---|---|---|
| Languages | JS, TS, JSX, TSX (via plugins: Vue, Svelte, MD, YAML) | JS, TS, JSX, TSX, JSON, CSS, GraphQL |
| Formatter included | No — pair with Prettier | Yes |
| Plugin ecosystem | Hundreds of plugins, 15+ years of rules | Limited (WASM plugins in v2, early) |
| Cold-start speed (1k files) | 5–15 s | 0.1–0.5 s |
| Config files | eslint.config.js + .prettierrc | biome.json |
| Type-aware linting | Yes (typescript-eslint) | No (parses syntax only) |
| React Hooks rules | Yes (eslint-plugin-react-hooks) | Partial (useExhaustiveDependencies) |
| Editor support | Mature — VS Code, IntelliJ, Vim | Good — 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,importthe config and spread/concat it into the array. parserOptions.projectmakes lint 10× slower — type-aware rules requiretsc-level type inference for every file. Scope to source only, or useprojectService: 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
.tsx—files: ["**/*.ts"]does NOT match.tsx. Usefiles: ["**/*.{ts,tsx}"]or["**/*.ts", "**/*.tsx"]. --fixmodifies generated files — make suredist/,build/,.next/, andcoverage/are in a globalignoresblock.no-unused-varsflags React imports — setvarsIgnorePattern: "^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. overridesis gone in flat config — what used to beoverrides: [{ files, rules }]is now a separate top-level object in the array. Don't translate the legacy shape literally.extendsis 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:
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:
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
// 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
# 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)
Print every rule actually applied to a file
npx eslint --print-config src/index.ts | jq '.rules | keys'
Output:
[
"@typescript-eslint/consistent-type-imports",
"@typescript-eslint/no-explicit-any",
"no-console",
"eqeqeq",
...
]
Generate a Markdown report from JSON output
npx eslint . --format json \
| jq -r '.[] | select(.errorCount + .warningCount > 0) | "- `\(.filePath | sub(".*/"; ""))` — \(.errorCount) errors, \(.warningCount) warnings"' \
> lint-report.md
Output (lint-report.md):
- `app.ts` — 1 errors, 0 warnings
- `helpers.ts` — 0 errors, 3 warnings
Disable a rule for one folder only
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-vitestfor test-file lints - TypeScript installation — required for
typescript-eslint