Skip to content

What Is ESLint? Rules, Flat Config, and Type-Aware Linting

ESLint gives JavaScript developers a pluggable static analysis utility to identify problematic code patterns and enforce logic consistency across projects.

Tuan Tran Van
12 min read
Contents (10 sections)
  1. What is ESLint?
  2. How linting differs from formatting and type checking
  3. Rules: the building block of ESLint
  4. eslint.config.js and the flat config system
  5. Plugins, parsers, and shareable configs
  6. How ESLint works with TypeScript
  7. Writing your own rule: walking the AST
  8. Running ESLint in your editor and CI
  9. What changed in ESLint v10
  10. References

ESLint is a configurable static analysis tool for your JavaScript projects.

It reads the source text and flags problematic patterns without ever executing the program, which is how logic errors and nonstandard implementations get caught in the editor instead of in a bug report. Nothing runs. Nothing has to be reproduced. The file is just text, and text can be inspected.

JavaScript earns a tool like this. It is dynamic and loosely typed, so a whole class of developer mistakes stays invisible until the exact line finally executes, and by then someone is usually watching. ESLint scans the files and answers immediately, which is the difference between a bug you fix while the context is still in your head and a bug you excavate three weeks later.

ESLint is runtime-agnostic and carries no opinion of its own, so every constraint it enforces is one you chose and wrote down.

ESLint reads JavaScript source and flags problematic patterns without ever running the program

What is ESLint?

Nicholas C. Zakas created ESLint in June 2013 as an open-source linter for JavaScript, and the governing idea was there from the first release: developers should be able to write and load their own rules dynamically, so the tool can move as fast as the language does. Linting is a form of static analysis that finds problematic patterns and code that drifts from a stated guideline. A compiler does its checking during a build step. ESLint works on a dynamic language that often has no build step to hide behind, which is exactly where the interesting mistakes live.

Underneath, ESLint runs on Node.js, which gives it a fast runtime for evaluating scripts. That choice makes installation boring in the good way: pull it from npm like anything else in your stack and run it as a standalone command or as one step in a build pipeline.

Every rule is a plugin. The rules that ship in the box are written with the same API the community gets, and I think that detail is underrated. The result is a linter with no agenda: ESLint promotes no coding style of its own, and the active rule list belongs to your project rather than to a style guide someone else voted on.

Rules and formatters are decoupled from the core engine, so you can point ESLint at additional logic at runtime. That separation is why the community can ship deep framework-specific analysis while the core project stays narrow and keeps its attention on generalizable, high-impact logic checks.

How linting differs from formatting and type checking

Three tools sit side by side in most JavaScript repositories, and they get confused constantly. A formatter cares about how the code looks: indentation, line length, where the comma lands. A linter cares about what the code does, which patterns it uses, and which of those tend to end badly. Both read your code without executing it. They are asking different questions.

Three layers of protection: the formatter handles appearance, the type checker handles data, and the linter catches logic errors and standard violations

Type checking, done by tools like TypeScript, keeps data structures and interfaces consistent across an application. But code can be perfectly type-safe and still be wrong. The no-unsafe-finally rule is the clean example: it stops you from writing a statement inside a finally block that quietly rewrites the program's control flow. Valid JavaScript. Legal types. Still a bug you will lose an afternoon to.

Run all three and you get three nets under the same codebase, each catching what the others structurally cannot: a formatter for visual appearance, a type checker for data integrity, and ESLint for best practices and latent runtime bugs. Three tools also means three configuration files to maintain, which is a real and slightly irritating cost. It is still cheaper than the bugs.

Rules: the building block of ESLint

A rule checks one expectation about your source code and decides what to do when the code fails it. ESLint sorts its findings into three groups: Possible Problems, which flag outright logic errors; Suggestions, which propose a better implementation; and Layout/Formatting, which handle stylistic conventions. The documentation marks problems with a checkmark (✅) and suggestions with a lightbulb (💡).

The anatomy of an ESLint rule and its three severity levels: off, warn, and error

You control the engine by configuring individual rules with their own options, starting with one of three severity levels:

  • "off" (0): The rule is disabled and no analysis is performed.
  • "warn" (1): The rule reports issues but does not trigger a non-zero exit code.
  • "error" (2): The rule reports issues as failures, which blocks a CI build or a commit.

Many rules can repair what they find, through the --fix CLI option. Those fixes (🔧) apply only when the correction cannot alter the underlying application logic. Suggestions (💡) are the more invasive category: they might change how the program behaves, so ESLint refuses to apply them for you. You approve each one manually through an editor extension. That caution is the right default. An autofix that silently changes behavior is worse than no autofix at all.

Bundled rules stay generalizable, and you enable them one at a time or through a predefined configuration. Whatever ends up running on your codebase is something your configuration explicitly sanctioned.

eslint.config.js and the flat config system

Flat config is the current standard for telling ESLint how to treat your project files. It lives in a file, usually eslint.config.js, that exports an array of configuration objects. Build them with the defineConfig helper imported from the eslint/config package, which gives you structural guidance and type safety instead of guesswork.

ESLint v10 looks for config starting from the linted file's own directory instead of the current working directory, letting several configs work at once in a monorepo

Each configuration object targets files and defines their environment through a few keys. The files key sets which globs or patterns the object applies to. The languageOptions key defines global variables and other environment-specific settings. The rules key is where you switch on and configure the rules those files need, and nothing outside that list gets checked.

ESLint stays neutral by default: it lints nothing until a configuration turns rules on or extends a shared configuration. Start with js/recommended for a baseline of high-impact logic checks. The explicitness is the point. When a file in a complicated directory tree is mysteriously not being analyzed, you have a config array to read rather than a black box to interrogate.

Plugins, parsers, and shareable configs

Three pieces do the extending. Plugins are npm modules that teach ESLint about specific libraries like React or frameworks like Angular, packaging rules, configurations, and processors together. A parser turns your source code into an Abstract Syntax Tree (AST), the structured representation every rule actually evaluates.

ESLint's three extension layers: plugins add rules, parsers turn code into an AST, and shareable configs apply an existing standard at once

By default ESLint parses standard JavaScript with Espree, and you can swap in a custom parser for non-standard syntax or newer language features. Espree and ESLint Scope now include built-in type definitions, which matters if you build custom linting tools or integrations on top of them. For code that lives inside a non-JS file, such as a JavaScript block in a Markdown document, a custom processor extracts that code so the rules can see it.

Shareable configurations ship through npm and let a team inherit a whole rule set at once. Extending a package like eslint-config-airbnb-base is the fastest way to get consistency across repositories, and also the fastest way to inherit several hundred opinions nobody on your team voted on. It saves project leads an enormous amount of maintenance work. It also guarantees that the first argument on some future pull request will be about a rule none of you chose.

Assembled together, these pieces produce an analysis setup shaped like your project: a parser for your language variant, plugins for your framework, a shareable configuration for the house style. The modularity is what keeps ESLint useful as the stack underneath it changes.

How ESLint works with TypeScript

Analyzing TypeScript starts with the @typescript-eslint/parser, which converts TypeScript syntax into something the rules can read, interpreting types, interfaces, and other language-specific features as part of the AST. Using ESLint's built-in TypeScript type definitions for your configuration requires TypeScript 5.3 or later.

Type information buys a deeper class of analysis than plain JavaScript linting can reach. With the types known, ESLint finds logical inconsistencies that are invisible without them, the kind of defect where every individual line looks fine and the combination does not.

Some options exist purely because TypeScript writes code differently. The max-params rule carries a countThis option. Set it to "never" and ESLint skips the this annotation in a function's argument list, treating it as metadata rather than a real parameter. Small thing, but it stops a type-annotation convention from tripping a parameter-count rule, so you never have to choose between clean type definitions and a passing lint run.

Writing your own rule: walking the AST

Custom rules are how you enforce the architectural standards that only exist inside your company, or kill a bug that keeps coming back in the same shape. A rule has two parts: meta and create. The meta object holds metadata such as the rule's type, its fixability, and the languages property, which for JavaScript must be set to ["js/js"] to declare compatibility. The create function holds the logic that walks the code and identifies violations.

Writing a custom rule: source is parsed into an AST, visitor methods walk each node, context.report flags the problem, and fix repairs it automatically

Inside create you follow the visitor pattern: return an object whose keys are ESTree node types, such as VariableDeclarator. As ESLint walks the AST it calls your function every time it meets a matching node, and you inspect that node's properties to decide whether it breaks your standard. Keep the Code Explorer open while you write one. It shows the ESTree representation of any snippet, and guessing node types from memory is how an hour disappears.

When you find a violation, call context.report() to flag it and give its location. If the rule can repair the problem, include a fixer object describing the transformation. This is the point where linting stops being generic best practice and starts encoding the specific things your own team keeps getting wrong.

Running ESLint in your editor and CI

The command line is the most direct entry point. npx eslint scans the directories or files you name and returns a detailed report of every violation. The CLI takes options including --fix, which resolves the remediable issues on the spot, and --color, which forces colored output in terminals that support it. That programmatic access is what makes everything downstream possible.

ESLint runs in three places: inline in the editor, through the command line, and as a CI gate that blocks bad code before it merges

Editor integrations shrink the feedback loop to almost nothing: squiggles appear under problematic code as you type, showing the exact rule you violated and offering the suggestions that need manual approval. Catching a logic error the second it enters the file, without leaving the editor, is worth more than any report generated later.

Rule authors get a RuleTester utility for checking custom logic against valid and invalid test cases. And in a CI pipeline the CLI becomes the gate: code carrying an error-level violation does not merge. That is the arrangement that keeps a production codebase honest, because the standard is enforced by something that cannot be talked out of it.

What changed in ESLint v10

Version 10 is a cleanup release with teeth. The legacy eslintrc system is finally gone: .eslintrc.* and .eslintignore files are no longer honored, the ESLINT_USE_FLAT_CONFIG environment variable has been removed, and the Linter constructor accepts only "flat" for its configType argument. The legacy CLI arguments went with it, including --no-eslintrc, --env, --resolve-plugins-relative-to, --rulesdir, and --ignore-path. /* eslint-env */ comments are now reported as errors rather than quietly ignored.

ESLint v10 removes eslintrc, the legacy CLI flags, and eslint-env comments, and raises the required Node.js version

Node.js requirements moved to ^20.19.0 || ^22.13.0 || >=24; v24.x was the LTS release when v10 shipped in February 2026, and versions 21.x and 23.x are not supported. The configuration lookup algorithm now starts from the directory of each linted file and walks upward, which fixes a genuine source of confusion in monorepos where several configuration files coexist.

Rule authoring got stricter too. RuleTester gained assertion options such as requireMessage, requireLocation, and requireData, so a test can insist on precise expectations instead of settling for "something was reported". The Program AST node's range now spans the entire source text, leading and trailing whitespace and comments included. If you write tooling that depends on exact range calculations for automated fixes, read that line twice.

Version 10 also tracks JSX references during scope analysis. Previously JSX identifiers were not counted as references at all, which produced false positives in no-unused-vars and false negatives in no-undef. Treating JSX elements as ordinary references to variables in scope means ESLint finally understands variable usage in a modern web project the way you always assumed it did.

References

Share this article