Overview
ESLint is the standard linting tool for JavaScript and TypeScript projects. It statically analyzes code to find problems, enforce coding conventions, and automatically fix many issues. ESLint uses a pluggable rule architecture where every rule is a standalone module, and developers can create and share their own rules and configurations.
ESLint 9 introduced a flat configuration system (eslint.config.js) that replaces the legacy .eslintrc format. The new config is simpler, uses native JavaScript modules, and provides better control over which files each rule applies to. ESLint integrates with all major editors and CI/CD pipelines.
Installation
# Initialize ESLint in a project (interactive setup)
npm init @eslint/config@latest
# Or install manually
npm install -D eslint
# Install TypeScript support
npm install -D typescript-eslint @typescript-eslint/parser
# Install popular plugins
npm install -D eslint-plugin-react eslint-plugin-react-hooks
npm install -D eslint-plugin-import
npm install -D eslint-plugin-jsx-a11y
# Global install (not recommended)
npm install -g eslint
Core Commands
| Command | Description |
|---|
npx eslint . | Lint all files in current directory |
npx eslint src/ | Lint specific directory |
npx eslint file.js | Lint a specific file |
npx eslint --fix . | Auto-fix fixable issues |
npx eslint --fix-dry-run . | Show what —fix would change |
npx eslint --debug . | Show debug information |
npx eslint --print-config file.js | Show resolved config for a file |
npx eslint --cache . | Use cache for faster re-runs |
npx eslint --max-warnings 0 . | Fail on any warnings |
npx eslint --format stylish . | Use specific output formatter |
# Built-in formatters
npx eslint --format stylish . # Default colored output
npx eslint --format json . # JSON output
npx eslint --format compact . # Single-line per issue
npx eslint --format html . # HTML report
npx eslint --format unix . # Unix-style output
Configuration
Flat Config (ESLint 9+)
// eslint.config.js
import js from "@eslint/js"
import globals from "globals"
import tseslint from "typescript-eslint"
import reactPlugin from "eslint-plugin-react"
import reactHooks from "eslint-plugin-react-hooks"
export default [
// Base recommended rules
js.configs.recommended,
// TypeScript rules
...tseslint.configs.recommended,
// Global settings
{
languageOptions: {
ecmaVersion: 2025,
sourceType: "module",
globals: {
...globals.browser,
...globals.node,
},
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
},
// React rules
{
plugins: {
react: reactPlugin,
"react-hooks": reactHooks,
},
rules: {
"react/react-in-jsx-scope": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
},
settings: {
react: { version: "detect" },
},
},
// Custom rules for all files
{
rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", {
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
}],
"no-console": "warn",
"prefer-const": "error",
eqeqeq: ["error", "always"],
},
},
// Ignore patterns
{
ignores: ["dist/", "node_modules/", "*.config.js", "coverage/"],
},
]
Legacy Config (.eslintrc)
{
"env": {
"browser": true,
"es2025": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"ecmaFeatures": { "jsx": true }
},
"plugins": ["@typescript-eslint", "react"],
"rules": {
"no-console": "warn",
"prefer-const": "error",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error"
},
"ignorePatterns": ["dist/", "node_modules/"]
}
Common Rules
Error Prevention
| Rule | Description |
|---|
no-undef | Disallow undeclared variables |
no-unused-vars | Disallow unused variables |
no-unreachable | Disallow unreachable code |
no-dupe-keys | Disallow duplicate keys in objects |
no-duplicate-case | Disallow duplicate case labels |
no-constant-condition | Disallow constant conditions in tests |
no-debugger | Disallow debugger statements |
use-isnan | Require isNaN() for NaN checks |
valid-typeof | Enforce valid typeof comparisons |
Best Practices
| Rule | Description |
|---|
eqeqeq | Require === and !== |
no-eval | Disallow eval() |
no-implied-eval | Disallow implied eval in setTimeout/setInterval |
no-var | Require let or const instead of var |
prefer-const | Prefer const for never-reassigned variables |
no-throw-literal | Require throwing Error objects |
no-return-await | Disallow unnecessary return await |
curly | Require curly braces for all control statements |
Rule Severity
// "off" or 0 — disable the rule
// "warn" or 1 — warn (does not affect exit code)
// "error" or 2 — error (exit code 1)
rules: {
"no-console": "warn", // or 1
"no-debugger": "error", // or 2
"no-alert": "off", // or 0
"semi": ["error", "always"], // rule with options
}
Advanced Usage
Inline Directives
/* eslint-disable no-console */
console.log("This line won't trigger a warning")
/* eslint-enable no-console */
// Disable for a single line
console.log("ok") // eslint-disable-line no-console
// Disable for the next line
// eslint-disable-next-line no-console
console.log("ok")
// Disable entire file
/* eslint-disable */
// Disable multiple rules
/* eslint-disable no-console, no-alert */
Custom Rules
// eslint-rules/no-foo.js
module.exports = {
meta: {
type: "suggestion",
docs: { description: "Disallow use of foo" },
fixable: "code",
schema: [],
},
create(context) {
return {
Identifier(node) {
if (node.name === "foo") {
context.report({
node,
message: "Avoid using 'foo'",
fix(fixer) {
return fixer.replaceText(node, "bar")
},
})
}
},
}
},
}
Per-File Configuration
// eslint.config.js
export default [
// Stricter rules for source code
{
files: ["src/**/*.{ts,tsx}"],
rules: {
"no-console": "error",
"@typescript-eslint/explicit-function-return-type": "error",
},
},
// Relaxed rules for tests
{
files: ["**/*.test.{ts,tsx}", "**/*.spec.{ts,tsx}"],
rules: {
"no-console": "off",
"@typescript-eslint/no-explicit-any": "off",
},
},
// Config files
{
files: ["*.config.{js,ts}"],
rules: {
"import/no-default-export": "off",
},
},
]
Integration with Prettier
# Install integration packages
npm install -D eslint-config-prettier eslint-plugin-prettier
// eslint.config.js
import prettier from "eslint-config-prettier"
export default [
// ... your configs
prettier, // Must be last to override formatting rules
]
CI/CD Integration
# .github/workflows/lint.yml
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx eslint --max-warnings 0 .
Troubleshooting
| Issue | Solution |
|---|
| Rules not applied | Run npx eslint --print-config file.js to see resolved config |
| Flat config not recognized | Ensure ESLint 9+; file must be eslint.config.js (not .eslintrc) |
| Plugin not found | Install the plugin package; check import in config file |
| TypeScript parsing errors | Install @typescript-eslint/parser; set parserOptions.project for type-aware rules |
| Slow linting | Use --cache flag; exclude node_modules and build directories |
| Conflicting with Prettier | Add eslint-config-prettier as the last config entry |
| Config migration from .eslintrc | Use npx @eslint/migrate-config .eslintrc.json for automatic migration |
| VS Code not showing errors | Install the ESLint extension; check output panel for errors |
| Cannot find module errors | Run npm install; check that all plugin packages are in devDependencies |