My optimal ESLint setup

After Airbnb’s TypeScript ESLint config was archived, I tested alternatives and found the perfect setup. Here’s my optimized ESLint configuration for TypeScript projects.

For a long time, I relied on eslint-config-airbnb-typescript. Unfortunately, in August 2024, the project was archived. Since then, the most popular fork on GitHub has only five stars (as of March 10, 2025). So, I decided it was time to switch to something else. After numerous trials, errors, and reading on Reddit and TwitterX, I believe I've found my sweet spot setup. Here’s a TL;DR version

The Configuration

required packages
Copied
@eslint/js eslint eslint-config-prettier eslint-plugin-import typescript-eslint
eslint.config.mjs
Copied
import js from '@eslint/js';
import eslintConfigPrettier from 'eslint-config-prettier';
import { flatConfigs as importConfigs } from 'eslint-plugin-import';
import tseslint from 'typescript-eslint';

export default tseslint.config([
  {
    ignores: ['var/'],
  },
  js.configs.recommended,
  importConfigs.recommended,
  importConfigs.typescript,
  tseslint.configs.recommendedTypeChecked,
  tseslint.configs.stylisticTypeChecked,
  eslintConfigPrettier,
  {
    languageOptions: {
      parserOptions: {
        projectService: {
          allowDefaultProject: ['*.config.mjs'],
        },
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },
  {
    rules: {
      // https://typescript-eslint.io/troubleshooting/typed-linting/performance/#eslint-plugin-import
      'import/named': 0,
      'import/namespace': 0,
      'import/default': 0,
      'import/no-named-as-default-member': 0,
      'import/no-unresolved': 0,
      'import/prefer-default-export': 0,

      'import/no-commonjs': 'error',
      'import/first': 'error',
      'import/newline-after-import': 'error',
      'import/order': [
        'error',
        {
          alphabetize: {
            order: 'asc',
          },
        },
      ],
      'import/no-extraneous-dependencies': [
        'error',
        {
          devDependencies: ['**/*.test.ts', 'tests/**/*.ts', '*.config.mjs'],
        },
      ],
      'import/no-cycle': ['error'],
      'import/no-duplicates': ['error', { 'prefer-inline': true }],
      'import/consistent-type-specifier-style': ['error', 'prefer-inline'],
    },
  },
  {
    rules: {
      '@typescript-eslint/consistent-type-definitions': 0,
      '@typescript-eslint/consistent-type-imports': [
        'error',
        { prefer: 'type-imports', fixStyle: 'inline-type-imports' },
      ],
      '@typescript-eslint/no-unused-vars': [
        'error',
        { argsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_' },
      ],
      'no-console': ['error', { allow: ['error', 'warn'] }],
      'class-methods-use-this': 0,
    },
  },
]);

Breaking It Down

javascript
Copied
  {
    ignores: ['var/'],
  },
  js.configs.recommended,
  importConfigs.recommended,
  importConfigs.typescript,
  tseslint.configs.recommendedTypeChecked,
  tseslint.configs.stylisticTypeChecked,

I use the /var directory for miscellaneous local files that aren’t part of the project - such as temporary snippets, local SQLite files, and caches. This habit comes from my experience with PHP and has stuck with me over the years.

Then, I include default safe configurations from ESLint, the import plugin, and TypeScript ESLint. I prioritize type-checked rules because I strongly advocate for strict typing - it provides a good balance between clean code and avoiding overly restrictive rules.

Prettier Configuration

plaintext
Copied
  eslintConfigPrettier,

This import ensures that Prettier handles formatting separately by disabling any conflicting style rules introduced by the previous configurations.

Types ESLint Rules

javascript
Copied
  {
    languageOptions: {
      parserOptions: {
        projectService: {
          allowDefaultProject: ['*.config.mjs'],
        },
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },

This configuration is required to use TypeScript ESLint’s typed rules. It also allows me to run ESLint on root-level config files like ESLint and Prettier configs.

Custom Rule Definitions

The first set of rules applies to the import plugin:

javascript
Copied
  {
    rules: {
      // https://typescript-eslint.io/troubleshooting/typed-linting/performance/#eslint-plugin-import
      'import/named': 0,
      'import/namespace': 0,
      'import/default': 0,
      'import/no-named-as-default-member': 0,
      'import/no-unresolved': 0,
      'import/prefer-default-export': 0,

      'import/no-commonjs': 'error',
      'import/first': 'error',
      'import/newline-after-import': 'error',
      'import/order': [
        'error',
        {
          alphabetize: {
            order: 'asc',
          },
        },
      ],
      'import/no-extraneous-dependencies': [
        'error',
        {
          devDependencies: ['**/*.test.ts', 'tests/**/*.ts', '*.config.mjs'],
        },
      ],
      'import/no-cycle': ['error'],
      'import/no-duplicates': ['error', { 'prefer-inline': true }],
      'import/consistent-type-specifier-style': ['error', 'prefer-inline'],
    },
  },

Some of these rules follow TypeScript ESLint recommendations. I also enforce strict import practices, such as requiring imports to be at the top of a file and preventing invalid imports.

Personal Preferences

javascript
Copied
  {
    rules: {
      '@typescript-eslint/consistent-type-definitions': 0,
      '@typescript-eslint/consistent-type-imports': [
        'error',
        { prefer: 'type-imports', fixStyle: 'inline-type-imports' },
      ],
      '@typescript-eslint/no-unused-vars': [
        'error',
        { argsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_' },
      ],
      'no-console': ['error', { allow: ['error', 'warn'] }],
      'class-methods-use-this': 0,
    },
  },

I prefer using type over interface, except when working with classes. I also allow unused variables prefixed with an underscore for flexibility.

This setup strikes a balance between strict linting and maintainability.

Special Case: Next.js

Next.js has its own rules and requires the FlatCompat class to function correctly. To accommodate this, install the @eslint/eslintrc package and modify your ESLint config as follows:

diff
Copied
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,14 +1,20 @@
+import { FlatCompat } from '@eslint/eslintrc';
 import js from '@eslint/js';
 import eslintConfigPrettier from 'eslint-config-prettier';
 import { flatConfigs as importConfigs } from 'eslint-plugin-import';
 import tseslint from 'typescript-eslint';

+const compat = new FlatCompat({
+  baseDirectory: import.meta.dirname,
+});
+
 export default tseslint.config([
   {
     ignores: ['var/'],
   },
   js.configs.recommended,
-  importConfigs.recommended,
+  ...compat.extends('next/core-web-vitals', 'next/typescript'),
+  { ...importConfigs.recommended, plugins: {} },
   importConfigs.typescript,
   tseslint.configs.recommendedTypeChecked,
   tseslint.configs.stylisticTypeChecked,

Next.js already defines configures import as a plugin so you have to omit it from the recommended config.