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
@eslint/js eslint eslint-config-prettier eslint-plugin-import typescript-eslint
# eslint.config.mjs
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
{
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
eslintConfigPrettier,
This import ensures that Prettier handles formatting separately by disabling any conflicting style rules introduced by the previous configurations.
Types ESLint Rules
# javascript
{
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
{
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
{
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
--- 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.