TechLead
Lesson 14 of 20
5 min read
DevTools & Productivity

Linting and Formatting

Configure ESLint and Prettier for TypeScript and React projects with custom rules, plugins, and automated workflows

Why Linting and Formatting Matter

Consistent code style and automated quality checks are foundational to maintainable codebases. ESLint catches bugs, enforces best practices, and prevents anti-patterns. Prettier handles formatting so developers never argue about tabs vs spaces, semicolons, or line length. Together, they create a safety net that catches issues before code reaches code review.

ESLint vs Prettier

  • ESLint: Analyzes code for quality issues (unused variables, missing dependencies in hooks, type errors, accessibility violations). Can fix some issues automatically.
  • Prettier: Reformats code for consistent style (indentation, line breaks, quotes, semicolons). Has no opinions about code quality - only formatting.
  • Together: Prettier handles formatting, ESLint handles quality. Use eslint-config-prettier to prevent conflicts between them.

ESLint Flat Config (eslint.config.js)

ESLint 9+ uses the new "flat config" format. This is a single JavaScript file that exports an array of configuration objects. It replaces the old .eslintrc format.

// eslint.config.js (ESLint 9+ flat config)
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import importPlugin from 'eslint-plugin-import';
import prettier from 'eslint-config-prettier';

export default tseslint.config(
  // Base JavaScript rules
  js.configs.recommended,

  // TypeScript rules
  ...tseslint.configs.recommendedTypeChecked,
  {
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },

  // React rules
  {
    plugins: {
      react,
      'react-hooks': reactHooks,
      'jsx-a11y': jsxA11y,
    },
    rules: {
      ...react.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      ...jsxA11y.configs.recommended.rules,
      'react/react-in-jsx-scope': 'off',
      'react/prop-types': 'off',
    },
    settings: {
      react: { version: 'detect' },
    },
  },

  // Import ordering
  {
    plugins: { import: importPlugin },
    rules: {
      'import/order': ['error', {
        groups: [
          'builtin', 'external', 'internal', 'parent', 'sibling', 'index'
        ],
        'newlines-between': 'always',
        alphabetize: { order: 'asc', caseInsensitive: true },
      }],
      'import/no-duplicates': 'error',
    },
  },

  // Custom rules
  {
    rules: {
      '@typescript-eslint/no-unused-vars': ['error', {
        argsIgnorePattern: '^_',
        varsIgnorePattern: '^_',
      }],
      '@typescript-eslint/no-explicit-any': 'warn',
      '@typescript-eslint/consistent-type-imports': ['error', {
        prefer: 'type-imports',
      }],
      'no-console': ['warn', { allow: ['warn', 'error'] }],
    },
  },

  // Disable formatting rules (Prettier handles formatting)
  prettier,

  // Ignore patterns
  {
    ignores: [
      'node_modules/**',
      '.next/**',
      'dist/**',
      'coverage/**',
      '*.config.js',
    ],
  }
);

Prettier Configuration

// .prettierrc
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 100,
  "bracketSpacing": true,
  "arrowParens": "always",
  "endOfLine": "lf",
  "jsxSingleQuote": false,
  "plugins": ["prettier-plugin-tailwindcss"],
  "tailwindConfig": "./tailwind.config.ts",
  "overrides": [
    {
      "files": "*.md",
      "options": {
        "printWidth": 80,
        "proseWrap": "always"
      }
    }
  ]
}
# .prettierignore
node_modules
.next
dist
coverage
pnpm-lock.yaml
package-lock.json

Running Linters

# ESLint commands
npx eslint .                         # Lint all files
npx eslint . --fix                   # Auto-fix fixable issues
npx eslint src/components/           # Lint specific directory
npx eslint --cache                   # Cache results for faster re-runs
npx eslint --debug                   # Show which config/rules are applied

# Prettier commands
npx prettier --check .               # Check formatting (CI-friendly)
npx prettier --write .               # Format all files
npx prettier --write "src/**/*.tsx"   # Format specific files

# Install dependencies
npm install -D eslint prettier typescript-eslint \
  eslint-config-prettier eslint-plugin-react \
  eslint-plugin-react-hooks eslint-plugin-jsx-a11y \
  eslint-plugin-import prettier-plugin-tailwindcss

VS Code Integration

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

EditorConfig

# .editorconfig - Editor-agnostic formatting basics
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab

Linting Best Practices

  • Fix, do not disable: When ESLint flags an issue, fix the code. Only add disable comments for genuine false positives, and always add a justification comment.
  • Use eslint-config-prettier: Always include this to prevent ESLint from conflicting with Prettier formatting rules.
  • Enable type-aware linting: TypeScript-ESLint with type checking catches bugs that regular ESLint cannot (like unchecked promises and type safety issues).
  • Run in CI: Always run eslint and prettier --check in your CI pipeline to catch issues before merge.
  • Start strict, relax later: Begin with strict configurations and disable specific rules only when the team agrees.

Continue Learning