ESLint catches bugs and enforces consistency in TypeScript projects. Here's how to set it up.
Basic Setup#
# Install dependencies
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
# For flat config (ESLint 9+)
npm install -D eslint @eslint/js typescript-eslintFlat Config (ESLint 9+)#
1// eslint.config.js
2import eslint from '@eslint/js';
3import tseslint from 'typescript-eslint';
4
5export default tseslint.config(
6 eslint.configs.recommended,
7 ...tseslint.configs.recommended,
8 {
9 languageOptions: {
10 parserOptions: {
11 project: true,
12 tsconfigRootDir: import.meta.dirname,
13 },
14 },
15 },
16 {
17 ignores: ['dist/', 'node_modules/', '*.config.js'],
18 }
19);
20
21// With strict type checking
22export default tseslint.config(
23 eslint.configs.recommended,
24 ...tseslint.configs.strictTypeChecked,
25 ...tseslint.configs.stylisticTypeChecked,
26 {
27 languageOptions: {
28 parserOptions: {
29 project: true,
30 tsconfigRootDir: import.meta.dirname,
31 },
32 },
33 }
34);Legacy Config (.eslintrc)#
1{
2 "root": true,
3 "parser": "@typescript-eslint/parser",
4 "parserOptions": {
5 "ecmaVersion": "latest",
6 "sourceType": "module",
7 "project": "./tsconfig.json"
8 },
9 "plugins": ["@typescript-eslint"],
10 "extends": [
11 "eslint:recommended",
12 "plugin:@typescript-eslint/recommended",
13 "plugin:@typescript-eslint/recommended-type-checked"
14 ],
15 "ignorePatterns": ["dist", "node_modules", "*.config.js"],
16 "rules": {
17 "@typescript-eslint/no-unused-vars": "error",
18 "@typescript-eslint/no-explicit-any": "warn"
19 }
20}React Projects#
1// eslint.config.js
2import eslint from '@eslint/js';
3import tseslint from 'typescript-eslint';
4import react from 'eslint-plugin-react';
5import reactHooks from 'eslint-plugin-react-hooks';
6
7export default tseslint.config(
8 eslint.configs.recommended,
9 ...tseslint.configs.recommended,
10 {
11 files: ['**/*.{ts,tsx}'],
12 plugins: {
13 react,
14 'react-hooks': reactHooks,
15 },
16 languageOptions: {
17 parserOptions: {
18 ecmaFeatures: { jsx: true },
19 project: true,
20 },
21 },
22 settings: {
23 react: { version: 'detect' },
24 },
25 rules: {
26 ...react.configs.recommended.rules,
27 ...reactHooks.configs.recommended.rules,
28 'react/react-in-jsx-scope': 'off',
29 'react/prop-types': 'off',
30 },
31 }
32);Essential Rules#
1// eslint.config.js
2export default [
3 // ... base config
4 {
5 rules: {
6 // TypeScript
7 '@typescript-eslint/no-unused-vars': ['error', {
8 argsIgnorePattern: '^_',
9 varsIgnorePattern: '^_',
10 }],
11 '@typescript-eslint/no-explicit-any': 'warn',
12 '@typescript-eslint/explicit-function-return-type': 'off',
13 '@typescript-eslint/explicit-module-boundary-types': 'off',
14 '@typescript-eslint/no-non-null-assertion': 'warn',
15
16 // Strict type-checked rules
17 '@typescript-eslint/no-floating-promises': 'error',
18 '@typescript-eslint/no-misused-promises': 'error',
19 '@typescript-eslint/await-thenable': 'error',
20 '@typescript-eslint/no-unnecessary-type-assertion': 'error',
21 '@typescript-eslint/prefer-nullish-coalescing': 'error',
22 '@typescript-eslint/prefer-optional-chain': 'error',
23
24 // Best practices
25 '@typescript-eslint/consistent-type-imports': ['error', {
26 prefer: 'type-imports',
27 }],
28 '@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
29 '@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
30 },
31 },
32];Custom Rule Configuration#
1// eslint.config.js
2export default [
3 {
4 rules: {
5 // Naming conventions
6 '@typescript-eslint/naming-convention': [
7 'error',
8 // Variables and functions: camelCase
9 {
10 selector: 'variable',
11 format: ['camelCase', 'UPPER_CASE', 'PascalCase'],
12 },
13 {
14 selector: 'function',
15 format: ['camelCase', 'PascalCase'],
16 },
17 // Types and interfaces: PascalCase
18 {
19 selector: 'typeLike',
20 format: ['PascalCase'],
21 },
22 // Private members: leading underscore
23 {
24 selector: 'memberLike',
25 modifiers: ['private'],
26 format: ['camelCase'],
27 leadingUnderscore: 'require',
28 },
29 // Boolean variables: prefixed
30 {
31 selector: 'variable',
32 types: ['boolean'],
33 format: ['PascalCase'],
34 prefix: ['is', 'has', 'should', 'can', 'will'],
35 },
36 ],
37
38 // Method signature style
39 '@typescript-eslint/method-signature-style': ['error', 'property'],
40
41 // No magic numbers
42 '@typescript-eslint/no-magic-numbers': ['warn', {
43 ignore: [0, 1, -1],
44 ignoreEnums: true,
45 ignoreReadonlyClassProperties: true,
46 }],
47 },
48 },
49];Disabling Rules#
1// Disable for entire file
2/* eslint-disable @typescript-eslint/no-explicit-any */
3
4// Disable for next line
5// eslint-disable-next-line @typescript-eslint/no-unused-vars
6const unusedVar = 'example';
7
8// Disable for specific line
9const result = data as any; // eslint-disable-line @typescript-eslint/no-explicit-any
10
11// Disable with reason
12// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Validated above
13const value = obj!.property;
14
15// Re-enable rule
16/* eslint-enable @typescript-eslint/no-explicit-any */Override Patterns#
1// eslint.config.js
2export default [
3 // Base config for all files
4 {
5 rules: {
6 '@typescript-eslint/no-explicit-any': 'error',
7 },
8 },
9
10 // Override for test files
11 {
12 files: ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**'],
13 rules: {
14 '@typescript-eslint/no-explicit-any': 'off',
15 '@typescript-eslint/no-non-null-assertion': 'off',
16 },
17 },
18
19 // Override for config files
20 {
21 files: ['*.config.ts', '*.config.js'],
22 rules: {
23 '@typescript-eslint/no-require-imports': 'off',
24 },
25 },
26
27 // Override for scripts
28 {
29 files: ['scripts/**/*.ts'],
30 rules: {
31 'no-console': 'off',
32 },
33 },
34];Prettier Integration#
# Install prettier and eslint config
npm install -D prettier eslint-config-prettier1// eslint.config.js
2import eslint from '@eslint/js';
3import tseslint from 'typescript-eslint';
4import prettier from 'eslint-config-prettier';
5
6export default tseslint.config(
7 eslint.configs.recommended,
8 ...tseslint.configs.recommended,
9 prettier, // Must be last to disable conflicting rules
10);1// .prettierrc
2{
3 "semi": true,
4 "singleQuote": true,
5 "tabWidth": 2,
6 "trailingComma": "es5",
7 "printWidth": 100
8}Import Sorting#
npm install -D eslint-plugin-import @typescript-eslint/parser1// eslint.config.js
2import importPlugin from 'eslint-plugin-import';
3
4export default [
5 {
6 plugins: {
7 import: importPlugin,
8 },
9 rules: {
10 'import/order': ['error', {
11 groups: [
12 'builtin',
13 'external',
14 'internal',
15 'parent',
16 'sibling',
17 'index',
18 'type',
19 ],
20 'newlines-between': 'always',
21 alphabetize: {
22 order: 'asc',
23 caseInsensitive: true,
24 },
25 }],
26 'import/no-duplicates': 'error',
27 'import/no-unresolved': 'off', // TypeScript handles this
28 },
29 settings: {
30 'import/resolver': {
31 typescript: true,
32 node: true,
33 },
34 },
35 },
36];IDE Integration#
1// .vscode/settings.json
2{
3 "editor.codeActionsOnSave": {
4 "source.fixAll.eslint": "explicit"
5 },
6 "eslint.validate": [
7 "javascript",
8 "javascriptreact",
9 "typescript",
10 "typescriptreact"
11 ],
12 "eslint.useFlatConfig": true
13}1// .vscode/extensions.json
2{
3 "recommendations": [
4 "dbaeumer.vscode-eslint",
5 "esbenp.prettier-vscode"
6 ]
7}Package.json Scripts#
1{
2 "scripts": {
3 "lint": "eslint .",
4 "lint:fix": "eslint . --fix",
5 "lint:check": "eslint . --max-warnings 0",
6 "format": "prettier --write .",
7 "format:check": "prettier --check ."
8 },
9 "lint-staged": {
10 "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
11 "*.{json,md}": "prettier --write"
12 }
13}Pre-commit Hooks#
npm install -D husky lint-staged
npx husky init# .husky/pre-commit
npx lint-staged1// package.json
2{
3 "lint-staged": {
4 "*.{ts,tsx}": [
5 "eslint --fix --max-warnings 0",
6 "prettier --write"
7 ]
8 }
9}CI Configuration#
1# .github/workflows/lint.yml
2name: Lint
3
4on: [push, pull_request]
5
6jobs:
7 lint:
8 runs-on: ubuntu-latest
9 steps:
10 - uses: actions/checkout@v4
11 - uses: actions/setup-node@v4
12 with:
13 node-version: '20'
14 cache: 'npm'
15
16 - run: npm ci
17 - run: npm run lint:check
18 - run: npm run format:checkMonorepo Setup#
1// eslint.config.js (root)
2import eslint from '@eslint/js';
3import tseslint from 'typescript-eslint';
4
5export default tseslint.config(
6 eslint.configs.recommended,
7 ...tseslint.configs.recommended,
8 {
9 languageOptions: {
10 parserOptions: {
11 project: ['./tsconfig.json', './packages/*/tsconfig.json'],
12 tsconfigRootDir: import.meta.dirname,
13 },
14 },
15 },
16 {
17 ignores: ['**/dist/', '**/node_modules/', '**/coverage/'],
18 }
19);Debugging ESLint#
1# Check config for specific file
2npx eslint --print-config src/index.ts
3
4# Debug why rule is failing
5npx eslint src/index.ts --debug
6
7# Check which files are being linted
8npx eslint . --debug 2>&1 | grep "Processing"
9
10# Show timing information
11TIMING=1 npx eslint .Best Practices#
Configuration:
✓ Use flat config for new projects
✓ Enable type-checked rules
✓ Integrate with Prettier
✓ Set up pre-commit hooks
Rules:
✓ Start with recommended, customize as needed
✓ Use consistent-type-imports
✓ Enable no-floating-promises
✓ Allow underscore for unused vars
Performance:
✓ Cache results in CI
✓ Use project references for monorepos
✓ Ignore generated files
✓ Run in parallel when possible
Conclusion#
ESLint with TypeScript catches type-related bugs and enforces consistency. Use flat config for new projects, enable strict type checking, and integrate with Prettier for formatting. Pre-commit hooks ensure code quality before commits reach the repository.