All topics
General · Learning hub

ESLint notes for developers

Master ESLint with a curated set of 1 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallTest yourself — ESLint quizMore General notes
ESLint

ESLint Essentials

ESLint Essentials ESLint is a static analysis tool for JavaScript and TypeScript that catches likely bugs, enforces consistent style, and flags anti-patterns be

ESLint Essentials

ESLint is a static analysis tool for JavaScript and TypeScript that catches likely bugs, enforces consistent style, and flags anti-patterns before code ever runs. It parses your source into an AST and runs a configurable set of rules against it — from "no unused variables" to "no floating promises" — turning problems that used to surface at runtime (or in review) into immediate, inline feedback in the editor and CI.

Setup & Flat Config

ESLint 9+ uses "flat config" — a single eslint.config.js exporting an array of config objects — replacing the older .eslintrc.* cascading-file format. Flat config is plain JavaScript (no special file-resolution magic), each object in the array applies to files matched by its files glob, and later objects override earlier ones for any overlapping keys.

// 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'

export default [
  js.configs.recommended,               // base JS rules
  ...tseslint.configs.recommended,      // spreads an array of TS configs

  {
    files: ['**/*.{ts,tsx}'],
    languageOptions: {
      parserOptions: {
        project: './tsconfig.json',     // enables type-aware rules
      },
    },
    plugins: {
      react: reactPlugin,
      'react-hooks': reactHooks,
    },
    rules: {
      'react-hooks/rules-of-hooks': 'error',
      'react-hooks/exhaustive-deps': 'warn',
      '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
      '@typescript-eslint/no-explicit-any': 'warn',
    },
  },

  {
    ignores: ['dist/**', 'node_modules/**', '.next/**'],  // replaces .eslintignore
  },
]

Under the legacy format (.eslintrc.json, still supported via ESLINT_USE_FLAT_CONFIG=false but deprecated), configuration cascades from nested .eslintrc files up the directory tree and uses extends/overrides keys instead of an ordered array — new projects should default to flat config.

Rules, Plugins & Extends

A rule checks one specific pattern (e.g. no-unused-vars). A plugin bundles related rules for a domain (eslint-plugin-react, @typescript-eslint) that aren't built into core ESLint. A shareable config (js.configs.recommended, or a package like eslint-config-airbnb) is a pre-assembled set of rule settings you extend instead of configuring every rule by hand.

{
  rules: {
    // Severity: 'off' | 'warn' | 'error' (or 0 | 1 | 2)
    'no-console': 'warn',

    // Rules that take options pass an array: [severity, ...options]
    'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],

    // Autofixable rules support `eslint --fix`
    'quotes': ['error', 'single', { avoidEscape: true }],

    // Type-aware rules from @typescript-eslint need `project` set in
    // languageOptions.parserOptions -- they see actual inferred types,
    // not just syntax, so they catch things plain AST rules can't.
    '@typescript-eslint/no-floating-promises': 'error',
    '@typescript-eslint/no-misused-promises': 'error',
  },
}

Rule severity has three levels: 'off' disables it, 'warn' reports without failing the build, 'error' fails eslint's exit code (and typically CI). Most teams run warn locally for style nits and error for anything that indicates an actual bug — an unhandled promise rejection, a hook called conditionally, an unreachable branch.

Parser Options & TypeScript Integration

ESLint's default parser (espree) only understands plain JavaScript. TypeScript files need @typescript-eslint/parser instead, which understands TS syntax and, when you point it at a tsconfig.json via parserOptions.project, gives type-aware rules access to the full type checker — enabling checks that need real type information, not just syntax shape.

// A type-aware rule catches things a syntax-only rule structurally cannot:

async function saveUser(user) {
  await db.users.save(user)
}

function handleClick() {
  saveUser(currentUser)   // forgot the `await` -- returns a Promise nobody awaits
}

// @typescript-eslint/no-floating-promises flags this because it knows,
// via the type checker, that saveUser() returns a Promise -- a plain
// syntax rule has no way to know that without evaluating types.

// languageOptions needed for type-aware rules to work at all:
export default [
  {
    languageOptions: {
      parser: tseslint.parser,
      parserOptions: {
        project: true,          // resolves the nearest tsconfig.json automatically
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },
]

Type-aware rules are significantly slower than syntax-only ones, since each file requires the TypeScript compiler to resolve its types across the project. For large codebases, many teams run the type-aware rule set only in CI or pre-commit, and a fast syntax-only subset on every keystroke in the editor.

Disabling Rules & Prettier Integration

Sometimes a rule is genuinely wrong for one specific line — a third-party type that forces an any, a console.log intentionally left in a CLI tool. Disable it narrowly with an inline comment rather than turning the rule off project-wide, and always leave a reason so a future reader (including you) knows it was a deliberate choice, not an oversight.

// Disables the rule for the next line only -- narrowest possible scope
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- legacy SDK has no types
const legacyResponse: any = thirdPartySdk.call()

// Disables for a whole block -- use sparingly, and re-enable after
/* eslint-disable no-console */
console.log('CLI output, not app logging')
console.log('this is fine here')
/* eslint-enable no-console */

// Never do this project-wide unless the rule genuinely doesn't apply
// to the whole codebase -- it silently drops coverage for every file.
// rules: { 'no-console': 'off' }

ESLint and Prettier solve different problems and shouldn't fight over the same territory: Prettier owns formatting (whitespace, line breaks, quote style), ESLint owns code quality (unused vars, unsafe patterns, best practices). Use eslint-config-prettier to turn off any ESLint formatting rules that would conflict with Prettier's output, and run them as two separate steps rather than trying to make ESLint's --fix reformat code Prettier already owns.

Practical Tips & Pitfalls

  • Run `eslint --fix` in a pre-commit hook (via lint-staged) so autofixable issues never reach a PR — it saves reviewer time for problems that actually need a human.

  • Don't reach for `eslint-disable` as a first response to a failing rule — read why the rule exists first; it's often catching a real bug, not being overly pedantic.

  • Keep the ignores array (or .eslintignore under legacy config) tight — accidentally linting node_modules or build output produces thousands of irrelevant errors that bury real ones.

  • Type-aware TypeScript rules require parserOptions.project to point at a real tsconfig — a common setup mistake is forgetting this and silently getting only syntax-level checks.

  • Enforce ESLint in CI, not just locally — a rule only editors see gets bypassed the moment someone commits from a machine without the extension installed.

  • Start new projects from a shared config (@eslint/js recommended, typescript-eslint recommended) and layer project-specific rules on top, rather than hand-picking every rule from zero.

Keep your ESLint knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever