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
| Package | Purpose |
|---|---|
eslint | The linter |
@eslint/js | ESLint’s recommended JS rules |
typescript-eslint | Parser, plugin, and configs |
typescript | Required for typed linting |
prettier | Code formatter |
eslint-config-prettier | Disables conflicting ESLint rules |
The Shared Configs
| Config | Purpose |
|---|---|
js.configs.recommended | ESLint recommended |
tseslint.configs.recommended | TypeScript recommended |
tseslint.configs.strict | More opinionated rules |
tseslint.configs.stylistic | Consistent styling |
recommendedTypeChecked | Type-aware recommended |
strictTypeChecked | Type-aware strict |
stylisticTypeChecked | Type-aware stylistic |
The Key Rules
| Rule | Catches |
|---|---|
no-floating-promises | Unhandled promises |
no-misused-promises | Promises in wrong places |
await-thenable | Awaiting non-promises |
no-unsafe-* | Any propagation |
strict-boolean-expressions | Truthy/falsy surprises |
switch-exhaustiveness-check | Missing switch cases |
consistent-type-imports | Type-only import style |
The Scripts
| Script | Command |
|---|---|
lint | eslint . |
lint:fix | eslint . --fix |
format | prettier --write . |
format:check | prettier --check . |
typecheck | tsc --noEmit |
The Parser Options
| Option | Purpose |
|---|---|
projectService: true | Modern typed linting (recommended) |
project: './tsconfig.json' | Legacy explicit project path |
tsconfigRootDir | Resolve 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Typed rules report parsing errors | projectService not configured | Add parserOptions.projectService: true |
| Prettier and ESLint conflict | Formatting rules not disabled | Add eslint-config-prettier last |
| Lint is slow | Typed linting on large project | Run in CI, not on save |
consistent-type-imports error | Missing config | Add rule to config |
| Config file type error | // @ts-check missing | Add directive at top |
| Plugin not found | typescript-eslint not installed | Install 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
| Item | Value |
|---|---|
| ESLint config format | Flat config (eslint.config.mjs) |
| Core packages | eslint, @eslint/js, typescript-eslint, typescript |
| Typed linting | parserOptions.projectService: true |
| Recommended config | tseslint.configs.recommendedTypeChecked |
| Strict config | tseslint.configs.strictTypeChecked |
| Prettier integration | eslint-config-prettier |
| Key typed rules | no-floating-promises, no-unsafe-*, switch-exhaustiveness-check |
| Formatting config | .prettierrc |
| CI commands | tsc --noEmit, eslint ., prettier --check . |
| Pre-commit | lint-staged with eslint --fix and prettier --write |
Key takeaways:
- ESLint 9 uses flat config;
typescript-eslintbundles the parser, plugin, and configs. Theeslint.config.mjsfile exports an array of configuration objects. ThedefineConfighelper provides type information and autocomplete . - Typed linting uses TypeScript’s type-checker for cross-file analysis. The
projectService: trueoption enables type-aware rules likeno-floating-promisesandno-unsafe-*. Typed linting is slower than syntactic linting, so it is best run in CI and pre-push hooks . - The
recommendedTypeCheckedconfig is the baseline for typed linting. It includes the recommended rules plus the type-aware rules. ThestrictTypeCheckedconfig adds more opinionated rules for projects that want maximum safety . - Prettier owns formatting; ESLint owns code quality. The
eslint-config-prettierpackage disables the ESLint rules that conflict with Prettier. The recommended setup uses separate commands for linting and formatting . - The
consistent-type-importsrule enforcesimport typefor 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 --noEmitcommand catches type errors. Theeslint .command catches code quality issues. Theprettier --check .command catches formatting issues. All three must pass before merging . - Pre-commit hooks with
lint-stagedautomate the checks. Only the staged files are linted and formatted, which keeps the commit fast. The.husky/pre-commithook runslint-stagedbefore 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!