ESLint Complete Guide | Flat Config, Rules, Plugins & TypeScript Integration

ESLint Complete Guide | Flat Config, Rules, Plugins & TypeScript Integration

이 글의 핵심

ESLint 9 ships a new flat config system (eslint.config.js) that replaces .eslintrc. This guide covers migration, TypeScript-ESLint setup, React rules, custom rules, Prettier integration, and enforcement in CI.

Setup: ESLint 9 Flat Config

# Initialize ESLint (generates eslint.config.js)
npm init @eslint/config@latest

# Or install manually
npm install -D eslint

Flat Config (eslint.config.js)

// eslint.config.js — ESLint 9 default format
import js from '@eslint/js';
import globals from 'globals';

export default [
  // Apply recommended JS rules globally
  js.configs.recommended,

  {
    // Files this config applies to
    files: ['**/*.{js,mjs,cjs,jsx}'],

    // Language options
    languageOptions: {
      ecmaVersion: 2024,
      sourceType: 'module',
      globals: {
        ...globals.browser,
        ...globals.node,
      },
    },

    // Custom rules
    rules: {
      'no-console': 'warn',
      'no-unused-vars': 'error',
      'eqeqeq': ['error', 'always'],
    },
  },

  // Ignore patterns
  {
    ignores: ['dist/**', 'node_modules/**', '*.min.js'],
  },
];

TypeScript Integration

npm install -D typescript-eslint
// eslint.config.js — with TypeScript
import js from '@eslint/js';
import tseslint from 'typescript-eslint';

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

  // TypeScript rules
  ...tseslint.configs.recommended,

  // Or stricter: tseslint.configs.strict

  {
    files: ['**/*.{ts,tsx}'],
    languageOptions: {
      parserOptions: {
        project: './tsconfig.json',         // Enable type-aware rules
        tsconfigRootDir: import.meta.dirname,
      },
    },
    rules: {
      // Type-aware rules (require project: tsconfig)
      '@typescript-eslint/no-floating-promises': 'error',  // Catch unhandled async
      '@typescript-eslint/no-misused-promises': 'error',
      '@typescript-eslint/await-thenable': 'error',

      // Overrides from recommended
      '@typescript-eslint/no-explicit-any': 'warn',        // Allow any with warning
      '@typescript-eslint/no-unused-vars': ['error', {
        argsIgnorePattern: '^_',    // Allow _prefix for unused args
        varsIgnorePattern: '^_',
      }],
    },
  },

  {
    ignores: ['dist/**', '*.js', '*.mjs'],
  },
);

React Integration

npm install -D eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y
// eslint.config.js — with React
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import tseslint from 'typescript-eslint';

export default tseslint.config(
  // ...

  {
    files: ['**/*.{jsx,tsx}'],
    plugins: {
      react,
      'react-hooks': reactHooks,
      'jsx-a11y': jsxA11y,
    },
    settings: {
      react: { version: 'detect' },   // Auto-detect React version
    },
    rules: {
      // React
      ...react.configs.recommended.rules,
      'react/react-in-jsx-scope': 'off',    // Not needed with React 17+
      'react/prop-types': 'off',            // Use TypeScript instead

      // Hooks rules
      ...reactHooks.configs.recommended.rules,
      'react-hooks/exhaustive-deps': 'warn',

      // Accessibility
      ...jsxA11y.configs.recommended.rules,
    },
  },
);

Key Rules Explained

Critical Rules (error level)

rules: {
  // Prevent common bugs
  'no-unused-vars': 'error',              // Remove unused variables
  'no-undef': 'error',                    // No undeclared variables
  'eqeqeq': ['error', 'always'],         // === instead of ==
  'no-var': 'error',                      // Use let/const not var

  // Async safety
  'no-floating-promises': 'error',        // Await all promises
  'require-await': 'error',              // Don't async without await

  // Security
  'no-eval': 'error',                    // No eval()
  'no-implied-eval': 'error',           // No setTimeout("code")
}

Quality Rules (warning level)

rules: {
  'no-console': ['warn', { allow: ['warn', 'error'] }],  // Allow console.warn/error
  'prefer-const': 'warn',                // Use const when not reassigned
  'no-shadow': 'warn',                   // Avoid variable shadowing
  'max-lines-per-function': ['warn', { max: 50 }],
}

Prettier Integration

npm install -D prettier eslint-config-prettier
// eslint.config.js — add prettier last (disables conflicting rules)
import prettier from 'eslint-config-prettier';

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

  prettier,  // Must be LAST — disables ESLint formatting rules
);
// .prettierrc
{
  "semi": true,
  "singleQuote": true,
  "trailingComma": "es5",
  "printWidth": 100,
  "tabWidth": 2
}
// package.json scripts
{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "format": "prettier --write .",
    "format:check": "prettier --check ."
  }
}

Inline Suppressions

// Disable for entire file
/* eslint-disable */

// Disable specific rule for file
/* eslint-disable @typescript-eslint/no-explicit-any */

// Disable for next line
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: any = JSON.parse(raw);

// Disable for a block
/* eslint-disable no-console */
console.log('intentional debug log');
/* eslint-enable no-console */

// ✅ Always add a comment explaining WHY
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// Third-party library returns untyped data — typed at boundary
const result: any = thirdPartyLib.getData();

Custom Rules

// rules/no-hardcoded-urls.js — custom rule example
export default {
  meta: {
    type: 'problem',
    docs: {
      description: 'Disallow hardcoded production URLs in source code',
    },
    schema: [],
  },

  create(context) {
    return {
      // Check all string literals
      Literal(node) {
        if (
          typeof node.value === 'string' &&
          node.value.includes('https://api.myapp.com')
        ) {
          context.report({
            node,
            message: 'Hardcoded production URL. Use process.env.API_URL instead.',
          });
        }
      },
    };
  },
};
// eslint.config.js — use custom rule
import noHardcodedUrls from './rules/no-hardcoded-urls.js';

export default [
  {
    plugins: {
      local: { rules: { 'no-hardcoded-urls': noHardcodedUrls } },
    },
    rules: {
      'local/no-hardcoded-urls': 'error',
    },
  },
];

CI Integration

# .github/workflows/lint.yml
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm run lint          # Fails if lint errors
      - run: npm run format:check  # Fails if formatting off

VS Code Integration

// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ]
}

Install extensions: ESLint (dbaeumer.vscode-eslint) + Prettier (esbenp.prettier-vscode)


Migration from .eslintrc to Flat Config

# Automated migration tool
npx @eslint/migrate-config .eslintrc.json
# Generates eslint.config.mjs
// .eslintrc.json (old)
{
  "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
  "plugins": ["@typescript-eslint"],
  "rules": { "no-console": "warn" }
}

// eslint.config.js (new)
import js from '@eslint/js';
import tseslint from 'typescript-eslint';

export default tseslint.config(
  js.configs.recommended,
  ...tseslint.configs.recommended,
  { rules: { 'no-console': 'warn' } }
);

Related posts: