DevToolBoxฟรี
บล็อก

ESLint 9 Complete Guide: Flat Config, TypeScript, and Modern Linting (2026)

19 min readโดย DevToolBox Team

ESLint is the most widely used static analysis tool for JavaScript and TypeScript. It identifies problematic patterns, enforces coding conventions, and catches bugs before they reach production. With the release of ESLint 9 and the new flat config system, the tool has undergone its most significant architectural change since its creation. Whether you are building a small script or maintaining a large monorepo, ESLint helps your team write consistent, high-quality code across every file.

TL;DR

ESLint 9 introduces flat config (eslint.config.js) replacing the legacy .eslintrc format. It provides native TypeScript-ESLint support, framework-specific plugins for React/Vue/Angular, custom rule authoring, shareable configs, auto-fix capabilities, and deep IDE integration. Flat config uses JavaScript modules for full programmatic control, simplifies config merging, and eliminates the confusing extends/overrides cascade.

Key Takeaways
  • ESLint 9 flat config (eslint.config.js) replaces .eslintrc with a simpler, more predictable JavaScript-based configuration system.
  • TypeScript-ESLint provides type-aware linting rules that catch errors the TypeScript compiler alone cannot detect.
  • ESLint auto-fix can automatically correct many issues including formatting, import sorting, and code style violations.
  • Framework plugins (React, Vue, Angular) add component-specific rules for hooks, template syntax, and lifecycle patterns.
  • Shareable configs like eslint-config-airbnb and @antfu/eslint-config let teams adopt battle-tested rule sets instantly.
  • ESLint handles linting while Prettier handles formatting. Biome offers both in a single faster tool but with a smaller plugin ecosystem.

What Is ESLint 9 and Flat Config?

ESLint is a pluggable static analysis tool for JavaScript and TypeScript that parses your source code into an abstract syntax tree (AST) and runs a set of rules against it. Each rule can report warnings or errors when it detects code patterns you want to avoid. ESLint 9, released in 2024, is the current major version and introduces flat config as the default configuration format.

Flat config replaces the legacy .eslintrc (JSON/YAML/JS) format with a single eslint.config.js (or .mjs/.cjs/.ts) file that exports an array of config objects. Each object specifies which files it applies to, which rules to enable, and which plugins to load. Config objects are merged in order from top to bottom, making the resolution logic transparent and predictable.

# Install ESLint 9
npm init @eslint/config@latest

# Or install manually
npm install --save-dev eslint @eslint/js

# This creates eslint.config.js (flat config)
# No more .eslintrc.json, .eslintrc.yml, or .eslintrc.js

ESLint vs Prettier vs Biome

Understanding how ESLint relates to Prettier and Biome helps you choose the right tool combination for your project. Here is a comparison of their core capabilities:

FeatureESLintPrettierBiome
Code quality rulesYes (hundreds)NoYes (growing)
Code formattingLimited (via rules)Yes (opinionated)Yes (Prettier-compatible)
TypeScript supportVia typescript-eslintBuilt-in parsingBuilt-in
Custom rulesYes (large ecosystem)NoLimited
SpeedModerateFastVery fast (Rust)
Plugin ecosystemLargestMinimalSmall but growing
Framework pluginsReact, Vue, Angular, SvelteN/AReact (partial)

The most common setup in 2026 is ESLint for code quality plus Prettier for formatting. Biome is an excellent all-in-one alternative if your project does not rely on framework-specific ESLint plugins.

Setting Up Flat Config (eslint.config.js)

Flat config is the default and only supported format in ESLint 9. The configuration file exports an array of config objects. Each object can specify files (glob patterns to include), ignores (glob patterns to exclude), plugins, rules, languageOptions, and settings.

Unlike the legacy format, there is no extends keyword. You import configs as JavaScript modules and spread them into your array. This eliminates the confusing cascade of extends, overrides, and env that plagued .eslintrc.

// eslint.config.js - Basic flat config
import js from "@eslint/js";

export default [
  // Global ignores (like .eslintignore)
  {
    ignores: ["dist/", "build/", "node_modules/", "*.min.js"],
  },

  // ESLint recommended rules for all JS files
  js.configs.recommended,

  // Custom overrides
  {
    files: ["**/*.js", "**/*.mjs"],
    rules: {
      "no-unused-vars": "warn",
      "no-console": ["error", { allow: ["warn", "error"] }],
      "prefer-const": "error",
      "no-var": "error",
      eqeqeq: ["error", "always"],
    },
  },
];

Rules and Severity Levels

Every ESLint rule has a severity level that determines how violations are reported. Understanding these levels is essential for configuring your project effectively.

  • "off" (or 0) - Disables the rule entirely. The rule is not run during linting.
  • "warn" (or 1) - Reports violations as warnings. The lint process exits successfully. Use this for rules you are migrating toward.
  • "error" (or 2) - Reports violations as errors. The lint process exits with a non-zero code. Use this for rules that must be enforced.

Rules can also accept configuration options as an array. The first element is the severity and subsequent elements are rule-specific settings. For example, ["error", "always"] enables a rule at error level with the "always" option.

// Rule configuration examples
export default [
  {
    rules: {
      // Severity only
      "no-debugger": "error",
      "no-alert": "warn",
      "no-eval": "off",

      // Severity + options
      "no-unused-vars": ["error", {
        argsIgnorePattern: "^_",
        varsIgnorePattern: "^_",
        caughtErrorsIgnorePattern: "^_",
      }],

      // Numeric severity (0=off, 1=warn, 2=error)
      "no-console": [1, { allow: ["warn", "error", "info"] }],

      // Complex options
      "no-restricted-imports": ["error", {
        patterns: [{
          group: ["lodash"],
          message: "Import from lodash-es instead for tree-shaking.",
        }],
      }],
    },
  },
];

TypeScript-ESLint Integration

TypeScript-ESLint is the official tooling that enables ESLint to lint TypeScript files. It provides a TypeScript parser for ESLint, a set of TypeScript-specific rules, and type-aware rules that leverage the TypeScript compiler API for deeper analysis.

Type-aware rules are the most powerful feature of TypeScript-ESLint. They use the full TypeScript type system to detect issues that neither the TypeScript compiler nor basic lint rules can catch, such as floating promises, unsafe any usage, and incorrect type assertions.

// eslint.config.js - TypeScript-ESLint setup
import js from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
  js.configs.recommended,

  // Recommended type-checked rules
  ...tseslint.configs.recommendedTypeChecked,

  // Stylistic type-checked rules (optional)
  ...tseslint.configs.stylisticTypeChecked,

  {
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },

  // Custom TypeScript rules
  {
    files: ["**/*.ts", "**/*.tsx"],
    rules: {
      "@typescript-eslint/no-floating-promises": "error",
      "@typescript-eslint/no-misused-promises": "error",
      "@typescript-eslint/strict-boolean-expressions": "warn",
      "@typescript-eslint/no-unnecessary-condition": "error",
      "@typescript-eslint/prefer-nullish-coalescing": "error",
    },
  },

  // Disable type-aware rules for JS files
  {
    files: ["**/*.js", "**/*.mjs"],
    ...tseslint.configs.disableTypeChecked,
  },
);

React, Vue, and Angular Plugins

ESLint has dedicated plugins for every major frontend framework. These plugins add rules that understand framework-specific patterns like React hooks, Vue template syntax, and Angular dependency injection.

React and JSX

The eslint-plugin-react and eslint-plugin-react-hooks packages provide rules for React components and hooks. The hooks plugin enforces the Rules of Hooks, which is critical for avoiding subtle bugs in function components.

// eslint.config.js - React + Hooks
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";

export default [
  js.configs.recommended,
  ...tseslint.configs.recommended,

  {
    files: ["**/*.tsx", "**/*.jsx"],
    plugins: {
      react,
      "react-hooks": reactHooks,
    },
    languageOptions: {
      parserOptions: {
        ecmaFeatures: { jsx: true },
      },
    },
    rules: {
      ...react.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      "react/react-in-jsx-scope": "off",
      "react/prop-types": "off",
      "react-hooks/exhaustive-deps": "warn",
    },
    settings: {
      react: { version: "detect" },
    },
  },
];

Vue.js

The eslint-plugin-vue package provides rules for Vue single-file components. It parses the template, script, and style sections and enforces Vue-specific conventions.

// eslint.config.js - Vue 3
import js from "@eslint/js";
import vue from "eslint-plugin-vue";
import vueParser from "vue-eslint-parser";
import tseslint from "typescript-eslint";

export default [
  js.configs.recommended,
  ...vue.configs["flat/recommended"],

  {
    files: ["**/*.vue"],
    languageOptions: {
      parser: vueParser,
      parserOptions: {
        parser: tseslint.parser,
        sourceType: "module",
      },
    },
    rules: {
      "vue/multi-word-component-names": "warn",
      "vue/no-unused-vars": "error",
      "vue/require-default-prop": "error",
    },
  },
];

Angular

The @angular-eslint packages replaced TSLint as the official linting solution for Angular projects. They provide rules for component decorators, dependency injection, template syntax, and lifecycle hooks.

// eslint.config.js - Angular
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import angular from "angular-eslint";

export default tseslint.config(
  js.configs.recommended,
  ...tseslint.configs.recommended,
  ...angular.configs.tsRecommended,

  {
    files: ["**/*.ts"],
    rules: {
      "@angular-eslint/directive-selector": ["error", {
        type: "attribute",
        prefix: "app",
        style: "camelCase",
      }],
      "@angular-eslint/component-selector": ["error", {
        type: "element",
        prefix: "app",
        style: "kebab-case",
      }],
    },
  },

  {
    files: ["**/*.html"],
    ...angular.configs.templateRecommended,
    ...angular.configs.templateAccessibility,
  },
);

Writing Custom Rules

ESLint rules are functions that receive an AST node and a context object. The context provides methods to report problems and access settings. Custom rules let you enforce project-specific conventions that no existing rule covers.

Each rule exports a meta object (with type, docs, schema, messages) and a create function that returns an object mapping AST node types to handler functions. When ESLint traverses the AST and encounters a matching node type, it calls your handler.

// eslint-plugin-custom/no-hardcoded-colors.js
export default {
  meta: {
    type: "suggestion",
    docs: {
      description: "Disallow hardcoded color hex values",
    },
    messages: {
      noHardcodedColor:
        "Avoid hardcoded color '{{ value }}'. Use a design token instead.",
    },
    schema: [],
  },

  create(context) {
    const HEX_PATTERN = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\$/;

    return {
      Literal(node) {
        if (
          typeof node.value === "string" &&
          HEX_PATTERN.test(node.value)
        ) {
          context.report({
            node,
            messageId: "noHardcodedColor",
            data: { value: node.value },
          });
        }
      },
    };
  },
};

// Using the custom rule in eslint.config.js
import noHardcodedColors from "./eslint-plugin-custom/no-hardcoded-colors.js";

export default [
  {
    plugins: {
      custom: { rules: { "no-hardcoded-colors": noHardcodedColors } },
    },
    rules: {
      "custom/no-hardcoded-colors": "warn",
    },
  },
];

Shareable Configs

Shareable configs are npm packages that export ESLint configuration arrays. They let teams adopt curated rule sets without manually configuring hundreds of rules. In flat config, you simply import and spread them into your config array.

Popular shareable configs in 2026 include @eslint/js (official recommended rules), typescript-eslint configs (strict, recommended, stylistic), @antfu/eslint-config (opinionated all-in-one), and eslint-config-airbnb (widely adopted style guide).

// eslint.config.js - Using @antfu/eslint-config
import antfu from "@antfu/eslint-config";

export default antfu({
  // Frameworks: auto-detected or manually enabled
  typescript: true,
  react: true,
  vue: false,

  // Formatting with Prettier-compatible style
  stylistic: {
    indent: 2,
    quotes: "single",
    semi: false,
  },

  // Override specific rules
  rules: {
    "no-console": "warn",
  },
});

// eslint.config.js - Composing multiple configs
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import prettierConfig from "eslint-config-prettier";

export default [
  js.configs.recommended,
  ...tseslint.configs.strict,
  prettierConfig,  // Must be last to override formatting rules
];

Ignoring Files and Directories

In flat config, you ignore files using the ignores property in a config object. A config object with only ignores acts as a global ignore pattern, similar to the old .eslintignore file. You can also combine ignores with files to exclude specific patterns from a config block.

// eslint.config.js - Ignoring patterns
export default [
  // Global ignores (standalone object = .eslintignore equivalent)
  {
    ignores: [
      "dist/",
      "build/",
      "coverage/",
      "node_modules/",
      "*.min.js",
      "**/*.generated.ts",
      ".next/",
      "public/",
    ],
  },

  // Scoped ignores: exclude test fixtures from strict rules
  {
    files: ["**/*.ts"],
    ignores: ["**/__fixtures__/**", "**/__mocks__/**"],
    rules: {
      "@typescript-eslint/no-explicit-any": "error",
    },
  },
];

CLI Usage and Common Commands

The ESLint CLI provides commands for linting, fixing, caching, and debugging configuration. Here are the most commonly used commands and flags for daily development workflows.

# Lint all files in the project
npx eslint .

# Lint specific files or directories
npx eslint src/ tests/
npx eslint "src/**/*.{ts,tsx}"

# Auto-fix all fixable issues
npx eslint . --fix

# Enable caching for faster re-runs
npx eslint . --cache --cache-location .eslintcache

# Fail on warnings (useful in CI)
npx eslint . --max-warnings 0

# Show which config applies to a file
npx eslint --inspect-config

# Print timing info for rules (find slow rules)
TIMING=1 npx eslint .

# Output results as JSON
npx eslint . --format json --output-file report.json

# Common package.json scripts
# "lint": "eslint ."
# "lint:fix": "eslint . --fix"
# "lint:ci": "eslint . --cache --max-warnings 0"

IDE Integration (VS Code)

The ESLint VS Code extension provides real-time linting feedback as you type. It shows errors and warnings inline, offers quick-fix actions, and can auto-fix on save. For flat config, the extension works automatically with no additional configuration.

Add these settings to your VS Code settings.json for the best experience with ESLint auto-fix on save and format-on-save integration with Prettier.

// .vscode/settings.json
{
  // Enable ESLint for these languages
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "vue"
  ],

  // Auto-fix ESLint issues on save
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },

  // Use Prettier as the default formatter
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,

  // ESLint uses flat config by default in v9
  // No additional setting needed for eslint.config.js
}

Auto-Fix and Formatting

ESLint can automatically fix many rule violations using the --fix flag. Fixable rules include import sorting, unused disable directives, spacing issues, and code style patterns. Not all rules are fixable since some require human judgment to resolve.

When using ESLint with Prettier, configure eslint-config-prettier to turn off ESLint rules that conflict with Prettier formatting. Then run Prettier for formatting and ESLint for code quality in your CI pipeline.

# Install Prettier + ESLint integration
npm install --save-dev prettier eslint-config-prettier

# eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import prettierConfig from "eslint-config-prettier";

export default [
  js.configs.recommended,
  ...tseslint.configs.recommended,

  // prettier must be LAST to turn off conflicting rules
  prettierConfig,
];

# CI pipeline: run both
# Step 1: Check formatting
# npx prettier --check .
# Step 2: Check code quality
# npx eslint . --max-warnings 0

Migrating from .eslintrc to Flat Config

ESLint provides a migration tool and compatibility utilities to help teams transition from the legacy .eslintrc format to flat config. The @eslint/eslintrc package provides a FlatCompat class that wraps legacy configs for use in flat config files.

For a clean migration, start by running the ESLint config migration tool, then gradually replace FlatCompat wrappers with native flat config equivalents as plugins release flat-config-compatible versions.

# Step 1: Run the migration tool
npx @eslint/migrate-config .eslintrc.json

# This generates eslint.config.mjs with FlatCompat wrappers
# for any legacy plugins that do not support flat config yet.

# Step 2: Example generated config with FlatCompat
# import { FlatCompat } from "@eslint/eslintrc";
# import path from "node:path";
# import { fileURLToPath } from "node:url";
#
# const __filename = fileURLToPath(import.meta.url);
# const __dirname = path.dirname(__filename);
# const compat = new FlatCompat({ baseDirectory: __dirname });
#
# export default [
#   ...compat.extends("eslint-config-airbnb"),
#   { rules: { "no-console": "warn" } },
# ];

# Step 3: Gradually replace FlatCompat with native imports
# as plugins release flat-config-compatible versions.
# Delete .eslintrc.json and .eslintignore when done.

Monorepo Configuration

In a monorepo, you typically have a root eslint.config.js that defines shared rules, plus package-specific overrides. Flat config makes this straightforward because you can import and compose config arrays from different packages.

Each workspace package can export its own config fragment that the root config imports and merges. This keeps framework-specific rules (React for the frontend, Node for the backend) in the packages that need them.

// eslint.config.js - Monorepo root config
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";

export default [
  // Global ignores
  { ignores: ["**/dist/", "**/node_modules/", "**/.next/"] },

  // Shared base rules for all packages
  js.configs.recommended,
  ...tseslint.configs.recommended,

  // Frontend: React rules
  {
    files: ["packages/web/**/*.tsx", "packages/web/**/*.ts"],
    plugins: { react, "react-hooks": reactHooks },
    rules: {
      ...react.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      "react/react-in-jsx-scope": "off",
    },
    settings: { react: { version: "detect" } },
  },

  // Backend: Node.js rules
  {
    files: ["packages/api/**/*.ts"],
    rules: {
      "no-console": "off",
    },
  },

  // Tests: relaxed rules
  {
    files: ["**/*.test.ts", "**/*.spec.ts"],
    rules: {
      "@typescript-eslint/no-explicit-any": "off",
      "@typescript-eslint/no-non-null-assertion": "off",
    },
  },
];

Performance Optimization

ESLint can be slow on large codebases, especially with type-aware TypeScript rules. Several techniques can dramatically reduce lint times.

  • Enable the --cache flag to skip re-linting unchanged files. The cache file stores lint results and file hashes.
  • Use project references with typescript-eslint to limit the type-checking scope for each package in a monorepo.
  • Exclude generated files, build output, and node_modules using global ignores in your config.
  • Disable type-aware rules for test files if the performance cost outweighs the benefit.
  • Use eslint-plugin-import with the resolver cache enabled to avoid redundant module resolution.
  • Run ESLint in parallel across workspaces using tools like turbo lint or nx lint.
  • Pin specific rule sets rather than using "all" configs that enable every available rule.
  • Profile slow rules with TIMING=1 npx eslint to identify and disable expensive rules in development.

Best Practices

Follow these recommendations to get the most value from ESLint in your project.

  • Start with a recommended config (eslint/js recommended + typescript-eslint strict) and customize from there rather than building from scratch.
  • Treat ESLint errors as CI blockers. Warnings should be temporary and tracked with a plan to promote them to errors.
  • Use ESLint for code quality and Prettier for formatting. Do not use ESLint formatting rules when Prettier is configured.
  • Keep your flat config file organized with comments separating base rules, TypeScript rules, framework rules, and test overrides.
  • Pin your ESLint and plugin versions. Major version upgrades can change rule behavior and break CI.
  • Use eslint-disable comments sparingly. Each one should include a reason explaining why the rule is disabled.
  • Configure your editor to auto-fix on save. This catches issues immediately and reduces CI failures.
  • Run ESLint in your CI pipeline with --max-warnings 0 to prevent warning accumulation.
  • Review new ESLint rules with each major release and adopt rules that match your team conventions.
  • Document any custom rules or non-obvious config choices in your project README or a dedicated linting guide.

Frequently Asked Questions

What is ESLint flat config?

Flat config is the new configuration format introduced in ESLint 9. It uses a single eslint.config.js file that exports an array of config objects. Each object specifies files, plugins, rules, and language options. Configs are merged top-to-bottom, replacing the complex extends/overrides cascade of the legacy .eslintrc format.

How do I use ESLint with TypeScript?

Install typescript-eslint and configure it in your eslint.config.js. The tseslint.config() helper provides recommended and strict rule presets. For type-aware rules, provide your tsconfig.json path in languageOptions.parserOptions.project. Type-aware rules use the TypeScript compiler API for deeper analysis.

Should I use ESLint or Prettier?

Use both. ESLint handles code quality rules (no-unused-vars, no-implicit-coercion, prefer-const) while Prettier handles formatting (indentation, line length, quotes). Install eslint-config-prettier to disable ESLint formatting rules that conflict with Prettier.

How do I migrate from .eslintrc to flat config?

Run npx @eslint/migrate-config .eslintrc.json to generate a flat config file. The tool wraps legacy plugins using the FlatCompat utility from @eslint/eslintrc. Gradually replace FlatCompat wrappers with native flat config imports as plugins add flat config support.

What is the difference between ESLint and Biome?

ESLint is a JavaScript-based linter with the largest plugin ecosystem, supporting every major framework. Biome is a Rust-based toolchain that combines linting and formatting in one fast binary. Biome is significantly faster but has fewer rules and limited framework plugin support compared to ESLint.

How do I auto-fix ESLint errors?

Run eslint --fix to automatically fix all fixable violations. In VS Code, enable "editor.codeActionsOnSave" with "source.fixAll.eslint" to auto-fix on every save. Not all rules are fixable. Rules that require human judgment report errors without providing an automatic fix.

How do I configure ESLint for a monorepo?

Place a root eslint.config.js with shared rules and use the files property to apply package-specific overrides. Each config object can target specific directories like "packages/frontend/**/*.tsx" for React rules or "packages/api/**/*.ts" for Node rules. Tools like Turborepo and Nx can run ESLint in parallel across workspaces.

How do I improve ESLint performance?

Enable caching with --cache to skip unchanged files. Exclude generated files and build output with global ignores. Limit type-aware rules to source files (not tests). Use TIMING=1 to profile slow rules. In monorepos, use project references to limit the TypeScript type-checking scope per package.

𝕏 Twitterin LinkedIn
บทความนี้มีประโยชน์ไหม?

อัปเดตข่าวสาร

รับเคล็ดลับการพัฒนาและเครื่องมือใหม่ทุกสัปดาห์

ไม่มีสแปม ยกเลิกได้ตลอดเวลา

ลองเครื่องมือที่เกี่ยวข้อง

{ }JSON FormatterJSJavaScript Minifier.*Regex Tester

บทความที่เกี่ยวข้อง

Vitest Complete Guide: Fast Unit Testing for Modern JavaScript & TypeScript (2026)

Comprehensive Vitest guide covering setup, test syntax, mocking, snapshot testing, code coverage, Vue/React component testing, TypeScript integration, Vitest UI, and migration from Jest.

TypeScript Type Guards: คู่มือการตรวจสอบประเภทขณะรันไทม์

เชี่ยวชาญ TypeScript type guards: typeof, instanceof, in และ guard แบบกำหนดเอง

React Design Patterns Guide: Compound Components, Custom Hooks, HOC, Render Props & State Machines

Complete React design patterns guide covering compound components, render props, custom hooks, higher-order components, provider pattern, state machines, controlled vs uncontrolled, composition, observer pattern, error boundaries, and module patterns.