Developer Tools

Configuring Prettier & ESLint for TypeScript/JavaScript Projects

Configuring Prettier ESLint For TypeScriptJavaScript Projects

Introduction

Maintaining consistent code style and quality across a development team is challenging, especially in large TypeScript or JavaScript projects with multiple contributors. Inconsistent formatting leads to noisy diffs, merge conflicts, and endless code review discussions about tabs versus spaces. Combining Prettier for automatic code formatting with ESLint for code quality enforcement provides an elegant solution that automates style consistency while catching bugs and enforcing best practices.

In this comprehensive guide, you’ll learn how to configure Prettier and ESLint together for modern TypeScript and JavaScript projects, including the new ESLint flat config format, integration with popular frameworks, CI/CD automation, and advanced customization patterns.

Understanding the Difference Between Prettier and ESLint

Although both tools improve code quality, they serve fundamentally different purposes. Understanding this distinction is crucial for proper configuration.

Prettier: The Opinionated Code Formatter

Prettier handles code formatting exclusively:

  • Line length and wrapping
  • Indentation (tabs vs. spaces)
  • Quotes (single vs. double)
  • Semicolons
  • Trailing commas
  • Bracket spacing
  • JSX formatting

ESLint: The Pluggable Linter

ESLint handles code correctness and quality:

  • Unused variables and imports
  • Undefined references
  • Type safety issues
  • Security vulnerabilities
  • Best practice violations
  • Framework-specific rules

Modern Setup with ESLint Flat Config

ESLint 9+ uses a new flat config format (eslint.config.js) that’s simpler and more powerful than the legacy .eslintrc format.

Step 1: Install Dependencies

# Core packages
npm install --save-dev eslint prettier

# TypeScript support
npm install --save-dev typescript-eslint @typescript-eslint/parser

# Prettier integration
npm install --save-dev eslint-config-prettier eslint-plugin-prettier

# Optional but recommended
npm install --save-dev eslint-plugin-import eslint-plugin-unused-imports

Step 2: Create ESLint Flat Config

// eslint.config.js - Modern Flat Config
import js from '@eslint/js';
import typescript from 'typescript-eslint';
import prettier from 'eslint-plugin-prettier/recommended';
import importPlugin from 'eslint-plugin-import';
import unusedImports from 'eslint-plugin-unused-imports';

export default typescript.config(
  // Base JavaScript rules
  js.configs.recommended,
  
  // TypeScript rules
  ...typescript.configs.recommended,
  ...typescript.configs.stylistic,
  
  // Prettier integration (must be last)
  prettier,
  
  // Global ignores
  {
    ignores: [
      'node_modules/**',
      'dist/**',
      'build/**',
      'coverage/**',
      '*.config.js',
      '*.config.mjs',
    ],
  },
  
  // Custom rules for all files
  {
    plugins: {
      'import': importPlugin,
      'unused-imports': unusedImports,
    },
    rules: {
      // Prettier enforced as error
      'prettier/prettier': 'error',
      
      // TypeScript rules
      '@typescript-eslint/no-unused-vars': 'off', // Handled by unused-imports
      '@typescript-eslint/no-explicit-any': 'warn',
      '@typescript-eslint/explicit-function-return-type': 'off',
      '@typescript-eslint/explicit-module-boundary-types': 'off',
      '@typescript-eslint/no-non-null-assertion': 'warn',
      '@typescript-eslint/consistent-type-imports': [
        'error',
        { prefer: 'type-imports' }
      ],
      
      // Import rules
      'import/order': [
        'error',
        {
          groups: [
            'builtin',
            'external',
            'internal',
            ['parent', 'sibling'],
            'index',
            'type',
          ],
          'newlines-between': 'always',
          alphabetize: { order: 'asc', caseInsensitive: true },
        },
      ],
      'import/no-duplicates': 'error',
      
      // Unused imports (auto-fixable)
      'unused-imports/no-unused-imports': 'error',
      'unused-imports/no-unused-vars': [
        'warn',
        {
          vars: 'all',
          varsIgnorePattern: '^_',
          args: 'after-used',
          argsIgnorePattern: '^_',
        },
      ],
      
      // General best practices
      'no-console': ['warn', { allow: ['warn', 'error'] }],
      'no-debugger': 'error',
      'prefer-const': 'error',
      'no-var': 'error',
      'eqeqeq': ['error', 'always'],
    },
  },
  
  // Test file overrides
  {
    files: ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**'],
    rules: {
      '@typescript-eslint/no-explicit-any': 'off',
      'no-console': 'off',
    },
  }
);

Step 3: Configure Prettier

// prettier.config.js - Prettier Configuration
/** @type {import('prettier').Config} */
export default {
  // Line width
  printWidth: 100,
  
  // Indentation
  tabWidth: 2,
  useTabs: false,
  
  // Quotes
  singleQuote: true,
  jsxSingleQuote: false,
  
  // Semicolons
  semi: true,
  
  // Trailing commas (ES5+ compatible)
  trailingComma: 'es5',
  
  // Brackets
  bracketSpacing: true,
  bracketSameLine: false,
  
  // Arrow functions
  arrowParens: 'always',
  
  // Line endings
  endOfLine: 'lf',
  
  // Prose wrapping for markdown
  proseWrap: 'preserve',
  
  // HTML whitespace sensitivity
  htmlWhitespaceSensitivity: 'css',
  
  // Embedded language formatting
  embeddedLanguageFormatting: 'auto',
  
  // Single attribute per line in HTML/JSX
  singleAttributePerLine: false,
  
  // Overrides for specific file types
  overrides: [
    {
      files: '*.json',
      options: {
        printWidth: 80,
        tabWidth: 2,
      },
    },
    {
      files: '*.md',
      options: {
        proseWrap: 'always',
        printWidth: 80,
      },
    },
    {
      files: '*.yaml',
      options: {
        tabWidth: 2,
        singleQuote: false,
      },
    },
  ],
};

Step 4: Configure Ignore Files

# .prettierignore - Files to skip formatting
node_modules/
dist/
build/
coverage/
*.min.js
*.min.css
package-lock.json
yarn.lock
pnpm-lock.yaml
.next/
.nuxt/
.output/

Step 5: Add NPM Scripts

{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "format": "prettier --write .",
    "format:check": "prettier --check .",
    "typecheck": "tsc --noEmit",
    "validate": "npm run typecheck && npm run lint && npm run format:check"
  }
}

Framework-Specific Configurations

React/Next.js Configuration

# Install React-specific plugins
npm install --save-dev eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y
// eslint.config.js - React Configuration
import js from '@eslint/js';
import typescript from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import prettier from 'eslint-plugin-prettier/recommended';

export default typescript.config(
  js.configs.recommended,
  ...typescript.configs.recommended,
  
  // React configuration
  {
    files: ['**/*.{jsx,tsx}'],
    plugins: {
      'react': react,
      'react-hooks': reactHooks,
      'jsx-a11y': jsxA11y,
    },
    languageOptions: {
      parserOptions: {
        ecmaFeatures: {
          jsx: true,
        },
      },
    },
    settings: {
      react: {
        version: 'detect',
      },
    },
    rules: {
      // React rules
      ...react.configs.recommended.rules,
      ...react.configs['jsx-runtime'].rules,
      'react/prop-types': 'off', // Using TypeScript
      'react/react-in-jsx-scope': 'off', // React 17+
      'react/jsx-no-target-blank': 'error',
      'react/jsx-curly-brace-presence': [
        'error',
        { props: 'never', children: 'never' }
      ],
      'react/self-closing-comp': 'error',
      'react/jsx-sort-props': [
        'warn',
        {
          callbacksLast: true,
          shorthandFirst: true,
          reservedFirst: true,
        },
      ],
      
      // React Hooks rules
      ...reactHooks.configs.recommended.rules,
      'react-hooks/rules-of-hooks': 'error',
      'react-hooks/exhaustive-deps': 'warn',
      
      // Accessibility rules
      ...jsxA11y.configs.recommended.rules,
      'jsx-a11y/anchor-is-valid': [
        'error',
        {
          components: ['Link'],
          specialLink: ['hrefLeft', 'hrefRight'],
          aspects: ['invalidHref', 'preferButton'],
        },
      ],
    },
  },
  
  prettier
);

Vue.js Configuration

# Install Vue-specific plugins
npm install --save-dev eslint-plugin-vue vue-eslint-parser
// eslint.config.js - Vue Configuration
import js from '@eslint/js';
import typescript from 'typescript-eslint';
import vue from 'eslint-plugin-vue';
import vueParser from 'vue-eslint-parser';
import prettier from 'eslint-plugin-prettier/recommended';

export default [
  js.configs.recommended,
  ...typescript.configs.recommended,
  
  // Vue configuration
  {
    files: ['**/*.vue'],
    plugins: {
      vue,
    },
    languageOptions: {
      parser: vueParser,
      parserOptions: {
        parser: typescript.parser,
        extraFileExtensions: ['.vue'],
        ecmaVersion: 'latest',
        sourceType: 'module',
      },
    },
    rules: {
      ...vue.configs['vue3-recommended'].rules,
      'vue/multi-word-component-names': 'off',
      'vue/no-v-html': 'warn',
      'vue/require-default-prop': 'off',
      'vue/component-tags-order': [
        'error',
        {
          order: ['script', 'template', 'style'],
        },
      ],
      'vue/block-lang': [
        'error',
        {
          script: { lang: 'ts' },
        },
      ],
      'vue/define-macros-order': [
        'error',
        {
          order: ['defineProps', 'defineEmits'],
        },
      ],
    },
  },
  
  prettier,
];

Node.js/Express Configuration

# Install Node-specific plugins
npm install --save-dev eslint-plugin-n eslint-plugin-security
// eslint.config.js - Node.js Configuration
import js from '@eslint/js';
import typescript from 'typescript-eslint';
import nodePlugin from 'eslint-plugin-n';
import security from 'eslint-plugin-security';
import prettier from 'eslint-plugin-prettier/recommended';

export default typescript.config(
  js.configs.recommended,
  ...typescript.configs.recommended,
  
  // Node.js configuration
  {
    plugins: {
      'n': nodePlugin,
      'security': security,
    },
    languageOptions: {
      globals: {
        ...nodePlugin.configs['recommended-module'].languageOptions.globals,
      },
    },
    rules: {
      // Node.js rules
      'n/no-missing-import': 'off', // TypeScript handles this
      'n/no-unsupported-features/es-syntax': 'off',
      'n/no-process-exit': 'error',
      'n/no-sync': 'warn',
      
      // Security rules
      ...security.configs.recommended.rules,
      'security/detect-object-injection': 'warn',
      'security/detect-non-literal-regexp': 'warn',
      'security/detect-possible-timing-attacks': 'warn',
    },
  },
  
  prettier
);

Editor Integration

VS Code Settings

// .vscode/settings.json - Workspace Settings
{
  // Format on save
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  
  // ESLint auto-fix on save
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
    "source.organizeImports": "never"
  },
  
  // Enable ESLint for all supported languages
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "vue",
    "html",
    "json"
  ],
  
  // Use flat config
  "eslint.useFlatConfig": true,
  
  // File associations
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[jsonc]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  
  // TypeScript settings
  "typescript.preferences.importModuleSpecifier": "relative",
  "typescript.suggest.autoImports": true
}
// .vscode/extensions.json - Recommended Extensions
{
  "recommendations": [
    "esbenp.prettier-vscode",
    "dbaeumer.vscode-eslint",
    "streetsidesoftware.code-spell-checker",
    "usernamehw.errorlens"
  ]
}

WebStorm/IntelliJ Settings

Settings → Languages & Frameworks → JavaScript → Prettier
  - Enable "On save"
  - Run for files: {**/*,*}.{js,ts,jsx,tsx,vue,json,css,scss,md}

Settings → Languages & Frameworks → JavaScript → Code Quality Tools → ESLint
  - Enable "Automatic ESLint configuration"
  - Enable "Run eslint --fix on save"

Git Hooks with Husky and lint-staged

# Install Husky and lint-staged
npm install --save-dev husky lint-staged

# Initialize Husky
npx husky init
// package.json - lint-staged configuration
{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,yaml,yml}": [
      "prettier --write"
    ],
    "*.{css,scss}": [
      "prettier --write"
    ]
  }
}
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged
# .husky/commit-msg - Optional commit message linting
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx --no -- commitlint --edit $1

CI/CD Integration

# .github/workflows/lint.yml - GitHub Actions
name: Lint and Format

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  lint:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run TypeScript check
        run: npm run typecheck
      
      - name: Run ESLint
        run: npm run lint
      
      - name: Check Prettier formatting
        run: npm run format:check
      
      - name: Run tests
        run: npm test
# GitLab CI equivalent
lint:
  stage: validate
  image: node:20
  cache:
    paths:
      - node_modules/
  script:
    - npm ci
    - npm run typecheck
    - npm run lint
    - npm run format:check
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
    - if: $CI_COMMIT_BRANCH == 'main'

Common Mistakes to Avoid

1. ESLint and Prettier Rule Conflicts

// WRONG - Conflicting formatting rules
export default [
  {
    rules: {
      'indent': ['error', 2],  // Conflicts with Prettier
      'quotes': ['error', 'single'],  // Conflicts with Prettier
      'semi': ['error', 'always'],  // Conflicts with Prettier
    },
  },
];

// CORRECT - Let Prettier handle formatting
import prettier from 'eslint-plugin-prettier/recommended';

export default [
  {
    rules: {
      // Only code quality rules, no formatting
      'no-unused-vars': 'warn',
      'prefer-const': 'error',
    },
  },
  prettier, // Disables conflicting rules and adds prettier/prettier
];

2. Not Using Type-Aware Linting

// WRONG - Missing TypeScript project reference
export default [
  ...typescript.configs.recommended,
];

// CORRECT - Enable type-aware rules
export default typescript.config(
  ...typescript.configs.recommendedTypeChecked,
  {
    languageOptions: {
      parserOptions: {
        project: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
  }
);

3. Ignoring Auto-Fixable Issues

# WRONG - Manually fixing formatting issues
git add .
git commit -m "fix formatting"

# CORRECT - Let tools auto-fix before commit
npm run lint:fix && npm run format
git add .
git commit -m "feat: add new feature"

4. Different Configs Across Environments

// WRONG - Different settings in CI vs local
// CI might pass while local fails

// CORRECT - Single source of truth
// All settings in eslint.config.js and prettier.config.js
// Same configs used locally, in CI, and in editors

Best Practices Summary

  • Use flat config: Migrate to ESLint’s new flat config format for simpler configuration.
  • Prettier last: Always add eslint-config-prettier last to disable conflicting rules.
  • Automate everything: Use lint-staged and Husky to enforce standards on every commit.
  • CI validation: Run lint and format checks in CI to catch issues before merge.
  • Editor integration: Configure VS Code or WebStorm for real-time feedback.
  • Framework plugins: Use appropriate plugins for React, Vue, or Node.js projects.
  • Type-aware rules: Enable TypeScript type-checking rules for deeper analysis.
  • Consistent team setup: Share .vscode/settings.json and extensions.json with your team.

Final Thoughts

Combining Prettier and ESLint gives your TypeScript and JavaScript projects both consistency and quality. You’ll save time in code reviews, reduce merge conflicts, and write cleaner, more maintainable code. The modern flat config format makes setup simpler than ever, and tight editor integration ensures developers get immediate feedback.

Once configured with git hooks and CI/CD pipelines, this setup requires minimal maintenance while providing massive productivity benefits. To take it further, read Using Git Hooks to Automate Code Quality Checks and Advanced TypeScript Types and Generics. For official documentation, check the Prettier configuration guide and ESLint flat config documentation.

1 Comment

Leave a Comment