| |

TypeScript 94 🔷 TypeScript with ESLint and Prettier

Writing TypeScript is only half the job. The compiler tells you when your types are wrong, but it says nothing about whether your code is unsafe, inconsistent, or difficult to maintain. ESLint catches the patterns the compiler misses: floating promises, unsafe any usage, unhandled errors, inconsistent naming. Prettier handles the formatting arguments nobody wants to have: tabs versus spaces, semicolons, line length. Together they enforce the discipline that TypeScript alone cannot.

The LFCA and Angular chapters in this series focus on runtime behavior and application structure. This chapter sits at the tooling layer — the configuration and workflow that keeps a TypeScript codebase consistent across developers and time. TypeScript 5.x and ESLint 9 have converged on a modern setup that uses flat configuration files, type-aware linting, and automated formatting. The setup is not complicated, but the details matter: the parser, the project service, the shared configs, and the interaction between linting and formatting.

Key point: ESLint 9 uses the flat config format (eslint.config.mjs). typescript-eslint provides the parser, plugin, and shareable configs. Type-aware linting uses TypeScript’s type-checker for cross-file analysis, enabling rules like no-floating-promises and no-unsafe-* that ESLint cannot perform syntactically . Typed linting is slower because it runs a TypeScript build before linting. IDE plugins cache and stay fast; teams typically run the full typed pass pre-push or in CI .


Why ESLint and Prettier matter for TypeScript

TypeScript’s compiler enforces types. It does not enforce patterns. A function that returns Promise<void> can be called without await, and the compiler will not complain. An any annotation disables type checking for an entire expression, and the compiler will not flag it. A switch statement that misses a case will compile, and the bug will surface at runtime.

The floating-promise problem. A Promise that is created and never awaited is a bug. The promise may reject with no handler, producing an unhandled rejection. The operation may still be running when the function returns, producing a race condition. TypeScript’s compiler does not flag this because the code is type-correct. @typescript-eslint/no-floating-promises catches it, but only when type-aware linting is enabled .

The unsafe-any problem. An any that flows into a function call, a property access, or a return statement is a hole in the type system. The compiler treats it as “trust me,” and every downstream operation loses type safety. @typescript-eslint/no-unsafe-* rules catch the specific ways any propagates — assignment, call, member access, return, argument — and report each one .

The consistency problem. Prettier removes the formatting decisions from code review. The .prettierrc file declares the rules: semicolons, single quotes, trailing commas, print width. Every file is formatted identically, and the diff only shows the actual changes. This is not about aesthetics. It is about reducing the noise in review so that real issues are visible.

The strict-type-checking problem. @typescript-eslint/strictTypeChecked enables rules that catch unreachable code, unnecessary conditions, and type mismatches that TypeScript’s strict mode does not flag. It is a superset of the recommended config, and it is what the community consensus recommends for new projects .

The trade-off. Typed linting requires a TypeScript project service, which means the linter must understand your tsconfig.json. On a large project, the first lint run takes seconds or minutes. Every subsequent run is cached. The cost is paid once, and the payoff is catching bugs that would otherwise reach production. For small projects, the syntactic rules alone may be enough. For anything that matters, the typed rules are the point .


a. Setting Up ESLint with Flat Config

ESLint 9 uses the flat config format. The legacy .eslintrc format is deprecated. The flat config is a JavaScript file that exports an array of configuration objects, each with files, languageOptions, and rules keys. The typescript-eslint package bundles the parser, plugin, and shareable configs into a single dependency .

The installation is four packages:

npm install --save-dev eslint @eslint/js typescript typescript-eslint

The typescript package is required even though typescript-eslint bundles its own TypeScript version, because the project service uses the project’s TypeScript installation for type information .

The minimal eslint.config.mjs enables the recommended rules for JavaScript and TypeScript:

// eslint.config.mjs
// @ts-check

import js from '@eslint/js';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';

export default defineConfig({
  files: ['**/*.{js,ts}'],
  extends: [js.configs.recommended, tseslint.configs.recommended],
});

The // @ts-check directive enables TypeScript to check the config file itself, which improves editor autocomplete and catches configuration errors . The defineConfig helper is built into ESLint 9 and provides type information for the config object. The files array scopes the rules to JavaScript and TypeScript files. The extends array applies the recommended configs from ESLint and typescript-eslint .

For projects that want stricter rules, the strict and stylistic configs are available:

export default defineConfig({
  files: ['**/*.{js,ts}'],
  extends: [
    js.configs.recommended,
    tseslint.configs.recommended,
    tseslint.configs.strict,
    tseslint.configs.stylistic,
  ],
});

The strict config is a superset of recommended that includes more opinionated rules. The stylistic config enforces consistent styling without catching bugs — it is for style, not safety. The stylisticTypeChecked and strictTypeChecked variants are the type-aware versions of these configs .


b. Enabling Typed Linting

Typed linting is what makes typescript-eslint different from a syntactic linter. It uses TypeScript’s type-checker to analyze your code with full type information, enabling rules that check for any propagation, floating promises, and incorrect type narrowing .

Enabling typed linting requires two changes. First, the presets are swapped to their TypeChecked variants. Second, the parser is pointed at the project’s tsconfig.json via projectService: true:

// eslint.config.mjs
// @ts-check

import js from '@eslint/js';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';

export default defineConfig({
  files: ['**/*.{js,ts}'],
  extends: [
    js.configs.recommended,
    tseslint.configs.recommendedTypeChecked,
    // or tseslint.configs.strictTypeChecked for stricter rules
  ],
  languageOptions: {
    parserOptions: {
      projectService: true,
      tsconfigRootDir: import.meta.dirname,
    },
  },
});

The projectService: true option is the modern replacement for project: './tsconfig.json'. It uses TypeScript’s project service APIs, which are the same APIs that power the editor’s type-checking. The tsconfigRootDir option ensures that the project service resolves the tsconfig.json relative to the config file, not the current working directory .

The difference between recommended and recommendedTypeChecked is significant. The recommended config includes rules that check syntax. The recommendedTypeChecked config includes those rules plus the type-aware rules. The strictTypeChecked config adds more opinionated rules on top, including the no-unsafe-* family .

The performance cost of typed linting is real. The first run on a large project may take minutes. The projectService option caches the type information, so subsequent runs are faster. IDE plugins run the linter incrementally and stay responsive. The recommended practice is to run the full typed lint pass in CI and on pre-push hooks, and rely on the IDE for fast feedback during development .


c. Integrating Prettier

ESLint and Prettier have overlapping responsibilities. ESLint can format code through the @typescript-eslint stylistic rules, and Prettier can format code. The community consensus is that Prettier should own formatting, and ESLint should own code quality. The eslint-config-prettier package disables the ESLint rules that conflict with Prettier, and eslint-plugin-prettier can run Prettier as an ESLint rule if desired .

The recommended setup is to install Prettier and eslint-config-prettier:

npm install --save-dev prettier eslint-config-prettier

The eslint-config-prettier package is a config that disables formatting rules. It is added to the extends array after the other configs:

// eslint.config.mjs
import js from '@eslint/js';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';

export default defineConfig({
  files: ['**/*.{js,ts}'],
  extends: [
    js.configs.recommended,
    tseslint.configs.recommendedTypeChecked,
    eslintConfigPrettier,
  ],
  languageOptions: {
    parserOptions: {
      projectService: true,
      tsconfigRootDir: import.meta.dirname,
    },
  },
});

The .prettierrc file configures Prettier’s behavior. A common configuration for TypeScript projects uses semicolons, single quotes, two-space indentation, ES5 trailing commas, and a 100-character print width :

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100
}

The prettier command formats files. The --check flag verifies formatting without writing changes, which is useful in CI. The --write flag formats and writes the files .

The eslint-plugin-prettier package is an alternative that runs Prettier as an ESLint rule. This means formatting errors appear in the same output as lint errors. The recommended config from eslint-plugin-prettier automatically includes eslint-config-prettier and enables the rule . For most projects, the separate Prettier command is simpler. For projects that want a single command for linting and formatting, the plugin integrates the two.

The package.json scripts tie everything together:

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

The lint script checks for lint errors. The lint:fix script applies automatic fixes. The format script formats all files. The format:check script verifies formatting in CI. The typecheck script runs TypeScript’s compiler in check-only mode .


Complete Example Session

This session builds a complete ESLint and Prettier setup for a TypeScript project, with typed linting and automated formatting.

// ============================================
// PART 1: THE PACKAGE INSTALLATION
// ============================================

// npm install --save-dev eslint @eslint/js typescript typescript-eslint prettier eslint-config-prettier

// ============================================
// PART 2: THE MINIMAL ESLINT CONFIG
// ============================================

// eslint.config.mjs
// @ts-check

import js from '@eslint/js';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';

export default defineConfig({
  files: ['**/*.{js,ts}'],
  extends: [js.configs.recommended, tseslint.configs.recommended],
});

// ============================================
// PART 3: THE TYPED LINTING CONFIG
// ============================================

// eslint.config.mjs
// @ts-check

import js from '@eslint/js';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';

export default defineConfig({
  files: ['**/*.{js,ts}'],
  extends: [
    js.configs.recommended,
    tseslint.configs.recommendedTypeChecked,
    eslintConfigPrettier,
  ],
  languageOptions: {
    parserOptions: {
      projectService: true,
      tsconfigRootDir: import.meta.dirname,
    },
  },
  rules: {
    '@typescript-eslint/no-unused-vars': [
      'error',
      { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
    ],
    '@typescript-eslint/consistent-type-imports': [
      'error',
      { fixStyle: 'inline-type-imports' },
    ],
  },
});

// ============================================
// PART 4: THE PRETTIER CONFIG
// ============================================

// .prettierrc
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100
}

// ============================================
// PART 5: THE TSCONFIG
// ============================================

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

// ============================================
// PART 6: THE PACKAGE SCRIPTS
// ============================================

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

// ============================================
// PART 7: THE FLOATING PROMISE RULE IN ACTION
// ============================================

// src/api.ts
export async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

// src/handler.ts
import { fetchUser } from './api';

export function handleRequest(id: string) {
  fetchUser(id); // ❌ no-floating-promises error
}

// The fix:
export async function handleRequest(id: string) {
  await fetchUser(id); // ✅ awaited
}

// ============================================
// PART 8: THE UNSAFE-ANY RULE IN ACTION
// ============================================

// src/parse.ts
export function parseData(data: any) {
  return data.value.toUpperCase(); // ❌ no-unsafe-member-access
}

// The fix:
export function parseData(data: unknown) {
  if (typeof data === 'object' && data !== null && 'value' in data) {
    const value = (data as { value: string }).value;
    return value.toUpperCase(); // ✅ type-safe
  }
  throw new Error('Invalid data');
}

// ============================================
// PART 9: THE CI PIPELINE
// ============================================

// .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm run typecheck
      - run: npm run lint
      - run: npm run format:check

// ============================================
// PART 10: THE PRE-COMMIT HOOK
// ============================================

// package.json
{
  "lint-staged": {
    "*.{js,ts,mjs,cjs}": ["eslint --fix", "prettier --write"],
    "*.{json,md,yaml,yml}": ["prettier --write"]
  }
}

// .husky/pre-commit
npm run lint-staged

The ten parts cover the package installation, the minimal ESLint config, the typed linting config, the Prettier config, the tsconfig.json, the package scripts, the floating-promise rule, the unsafe-any rule, the CI pipeline, and the pre-commit hook.


Quick Reference

The Core Packages

PackagePurpose
eslintThe linter
@eslint/jsESLint’s recommended JS rules
typescript-eslintParser, plugin, and configs
typescriptRequired for typed linting
prettierCode formatter
eslint-config-prettierDisables conflicting ESLint rules

The Shared Configs

ConfigPurpose
js.configs.recommendedESLint recommended
tseslint.configs.recommendedTypeScript recommended
tseslint.configs.strictMore opinionated rules
tseslint.configs.stylisticConsistent styling
recommendedTypeCheckedType-aware recommended
strictTypeCheckedType-aware strict
stylisticTypeCheckedType-aware stylistic

The Key Rules

RuleCatches
no-floating-promisesUnhandled promises
no-misused-promisesPromises in wrong places
await-thenableAwaiting non-promises
no-unsafe-*Any propagation
strict-boolean-expressionsTruthy/falsy surprises
switch-exhaustiveness-checkMissing switch cases
consistent-type-importsType-only import style

The Scripts

ScriptCommand
linteslint .
lint:fixeslint . --fix
formatprettier --write .
format:checkprettier --check .
typechecktsc --noEmit

The Parser Options

OptionPurpose
projectService: trueModern typed linting (recommended)
project: './tsconfig.json'Legacy explicit project path
tsconfigRootDirResolve config relative to file

Best Practices

✅ Do This:

// Use projectService for typed linting
parserOptions: { projectService: true }                    // ✅
// Add eslint-config-prettier after other configs
extends: [js.configs.recommended, tseslint.configs.recommended, eslintConfigPrettier] // ✅
// Scope rules to file types
files: ['**/*.{js,ts}']                                    // ✅
// Run format:check in CI
"format:check": "prettier --check ."                       // ✅
// Run typecheck alongside lint
"typecheck": "tsc --noEmit"                                // ✅

❌ Don’t Do This:

// Don't use legacy .eslintrc format
// ESLint 9 uses flat config.                              // ❌
// Don't enable typed rules without projectService
tseslint.configs.recommendedTypeChecked // without parserOptions // ❌
// Don't let ESLint and Prettier fight over formatting
// Use eslint-config-prettier to disable conflicts.        // ❌
// Don't skip format:check in CI
// Unformatted code slips through.                        // ❌

Common Pitfalls

PitfallWhy It HappensFix
Typed rules report parsing errorsprojectService not configuredAdd parserOptions.projectService: true
Prettier and ESLint conflictFormatting rules not disabledAdd eslint-config-prettier last
Lint is slowTyped linting on large projectRun in CI, not on save
consistent-type-imports errorMissing configAdd rule to config
Config file type error// @ts-check missingAdd directive at top
Plugin not foundtypescript-eslint not installedInstall as dev dependency

Real-World Examples

1. Minimal ESLint Config

export default defineConfig({
  files: ['**/*.{js,ts}'],
  extends: [js.configs.recommended, tseslint.configs.recommended],
});

2. Typed Linting Config

extends: [js.configs.recommended, tseslint.configs.recommendedTypeChecked],
languageOptions: { parserOptions: { projectService: true } },

3. Prettier Config

{ "semi": true, "singleQuote": true, "printWidth": 100 }

4. ESLint Prettier Integration

import eslintConfigPrettier from 'eslint-config-prettier';
extends: [eslintConfigPrettier]

5. Floating Promise Rule

// ❌ fetchUser(id);
// ✅ await fetchUser(id);

6. Unsafe Any Rule

// ❌ function parse(data: any) { return data.value; }
// ✅ function parse(data: unknown) { ... }

7. Type-Only Import Rule

import type { User } from './types';

8. CI Pipeline

- run: npm run typecheck
- run: npm run lint
- run: npm run format:check

9. Pre-Commit Hook

"lint-staged": { "*.{js,ts}": ["eslint --fix", "prettier --write"] }

10. Package Scripts

"lint": "eslint .", "format": "prettier --write .", "typecheck": "tsc --noEmit"

Visual

The ESLint 9 Flat Config Structure

┌──────────────────────────────────────────────┐
│  eslint.config.mjs                           │
│                                              │
│  export default defineConfig({               │
│    files: ['**/*.{js,ts}'],  ← scope         │
│    extends: [                ← configs       │
│      js.configs.recommended,                 │
│      tseslint.configs.recommendedTypeChecked,│
│      eslintConfigPrettier,                   │
│    ],                                        │
│    languageOptions: {                        │
│      parserOptions: {                        │
│        projectService: true, ← typed linting │
│      },                                      │
│    },                                        │
│    rules: { ... },           ← overrides     │
│  });                                         │
│                                              │
└──────────────────────────────────────────────┘

The Typed Linting Flow

┌──────────────────────────────────────────────┐
│  TYPED LINTING FLOW                          │
│                                              │
│  Source code ──> TypeScript project service  │
│       │                    │                 │
│       │                    ▼                 │
│       │              Type information        │
│       │                    │                 │
│       ▼                    ▼                 │
│  ESLint ──> Rules with type awareness        │
│       │                                      │
│       ▼                                      │
│  Reports: no-floating-promises,              │
│           no-unsafe-*, strict-boolean-       │
│           expressions, switch-exhaustiveness │
│                                              │
│  Requires: parserOptions.projectService      │
│                                              │
└──────────────────────────────────────────────┘

The ESLint and Prettier Split

┌──────────────────────────────────────────────┐
│  ESLINT vs PRETTIER                          │
│                                              │
│  ESLint:                                     │
│    ├─ Code quality                           │
│    ├─ Bug detection                          │
│    ├─ Type safety rules                      │
│    └─ Consistent patterns                    │
│                                              │
│  Prettier:                                   │
│    ├─ Formatting only                        │
│    ├─ Whitespace, quotes, semicolons         │
│    ├─ Line width                             │
│    └─ Zero configuration debates             │
│                                              │
│  eslint-config-prettier:                     │
│    └─ Disables ESLint rules that conflict    │
│                                              │
└──────────────────────────────────────────────┘

The CI Check Pipeline

┌──────────────────────────────────────────────┐
│  CI PIPELINE                                 │
│                                              │
│  npm ci                                      │
│       │                                      │
│       ▼                                      │
│  npm run typecheck  ← tsc --noEmit           │
│       │                                      │
│       ▼                                      │
│  npm run lint       ← eslint .               │
│       │                                      │
│       ▼                                      │
│  npm run format:check ← prettier --check .   │
│       │                                      │
│       ▼                                      │
│  All pass → merge allowed                    │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
ESLint config formatFlat config (eslint.config.mjs)
Core packageseslint, @eslint/js, typescript-eslint, typescript
Typed lintingparserOptions.projectService: true
Recommended configtseslint.configs.recommendedTypeChecked
Strict configtseslint.configs.strictTypeChecked
Prettier integrationeslint-config-prettier
Key typed rulesno-floating-promises, no-unsafe-*, switch-exhaustiveness-check
Formatting config.prettierrc
CI commandstsc --noEmit, eslint ., prettier --check .
Pre-commitlint-staged with eslint --fix and prettier --write

Key takeaways:

  • ESLint 9 uses flat config; typescript-eslint bundles the parser, plugin, and configs. The eslint.config.mjs file exports an array of configuration objects. The defineConfig helper provides type information and autocomplete .
  • Typed linting uses TypeScript’s type-checker for cross-file analysis. The projectService: true option enables type-aware rules like no-floating-promises and no-unsafe-*. Typed linting is slower than syntactic linting, so it is best run in CI and pre-push hooks .
  • The recommendedTypeChecked config is the baseline for typed linting. It includes the recommended rules plus the type-aware rules. The strictTypeChecked config adds more opinionated rules for projects that want maximum safety .
  • Prettier owns formatting; ESLint owns code quality. The eslint-config-prettier package disables the ESLint rules that conflict with Prettier. The recommended setup uses separate commands for linting and formatting .
  • The consistent-type-imports rule enforces import type for type-only imports. This separates runtime imports from compile-time imports, improving tree-shaking and making the code clearer .
  • The CI pipeline runs type checking, linting, and format checking. The tsc --noEmit command catches type errors. The eslint . command catches code quality issues. The prettier --check . command catches formatting issues. All three must pass before merging .
  • Pre-commit hooks with lint-staged automate the checks. Only the staged files are linted and formatted, which keeps the commit fast. The .husky/pre-commit hook runs lint-staged before every commit .

Remember: ESLint and Prettier are the tools that enforce the discipline TypeScript’s compiler cannot. ESLint catches floating promises, unsafe any usage, and inconsistent patterns. Prettier handles the formatting arguments. The setup uses flat config, typed linting via the project service, and eslint-config-prettier to keep the tools from fighting. Run type checking, linting, and format checking in CI. Automate the checks with pre-commit hooks. The goal is not to satisfy the linter. The goal is to catch the bugs that types alone miss.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!