Episode 1 — Fundamentals / 1.25 — TypeScript Essentials

1.25.l — Linting and Formatting

In one sentence: ESLint with TypeScript-specific plugins catches code quality issues the compiler misses, Prettier enforces consistent formatting, and tools like husky and lint-staged automate these checks before code is committed.

Navigation: ← 1.25 Overview · 1.25-Exercise-Questions →


1. ESLint with TypeScript

ESLint is the standard JavaScript/TypeScript linter. For TypeScript projects, you need two additional packages:

  • @typescript-eslint/parser — teaches ESLint to understand TypeScript syntax
  • @typescript-eslint/eslint-plugin — provides TypeScript-specific linting rules
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

Why ESLint on top of TypeScript?

TypeScript catches type errors. ESLint catches code quality issues that the type system does not:

TypeScript catchesESLint catches
Wrong type assignmentsUnused variables (with auto-fix)
Missing propertiesInconsistent patterns
Incorrect function argumentsPotential bugs (no-fallthrough in switch)
Null/undefined accessCode style (prefer-const, no-var)
TypeScript-specific bad practices (no-explicit-any)
Accessibility issues (with eslint-plugin-jsx-a11y)

2. Setting up ESLint for TypeScript

Modern flat config (ESLint 9+)

Create eslint.config.js:

import eslint from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  {
    languageOptions: {
      parserOptions: {
        project: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },
  {
    ignores: ["dist/", "node_modules/"],
  }
);

Legacy config (ESLint 8 and older)

Create .eslintrc.json:

{
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./tsconfig.json",
    "ecmaVersion": 2020,
    "sourceType": "module"
  },
  "plugins": ["@typescript-eslint"],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "rules": {},
  "ignorePatterns": ["dist/", "node_modules/"]
}

Running ESLint

# Lint all TypeScript files in src/
npx eslint src/ --ext .ts,.tsx

# Lint and auto-fix what can be fixed
npx eslint src/ --ext .ts,.tsx --fix

# Add to package.json scripts
{
  "scripts": {
    "lint": "eslint src/ --ext .ts,.tsx",
    "lint:fix": "eslint src/ --ext .ts,.tsx --fix"
  }
}

3. Key TypeScript-specific ESLint rules

RuleWhat it doesRecommendation
@typescript-eslint/no-explicit-anyWarns on any usageError — forces proper typing
@typescript-eslint/no-unused-varsWarns on unused variablesError — catches dead code
@typescript-eslint/no-non-null-assertionWarns on ! operatorWarn — prefer proper null checks
@typescript-eslint/explicit-function-return-typeRequires return type annotationsOff or warn — inference is often enough
@typescript-eslint/strict-boolean-expressionsDisallows truthy/falsy checks on non-booleansWarn — catches subtle bugs
@typescript-eslint/no-floating-promisesRequires handling of PromisesError — catches unhandled async
@typescript-eslint/no-misused-promisesCatches Promises used incorrectlyError — prevents async bugs
@typescript-eslint/prefer-nullish-coalescingSuggests ?? over || for nullish valuesWarn — more correct
@typescript-eslint/prefer-optional-chainSuggests ?. over manual checksWarn — cleaner code
@typescript-eslint/consistent-type-importsEnforces import type for type-only importsError — cleaner imports

Example configuration with specific rules

{
  "rules": {
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/no-unused-vars": ["error", {
      "argsIgnorePattern": "^_",
      "varsIgnorePattern": "^_"
    }],
    "@typescript-eslint/no-non-null-assertion": "warn",
    "@typescript-eslint/no-floating-promises": "error",
    "@typescript-eslint/consistent-type-imports": ["error", {
      "prefer": "type-imports"
    }],
    "@typescript-eslint/prefer-nullish-coalescing": "warn",
    "@typescript-eslint/prefer-optional-chain": "warn"
  }
}

What these rules catch

// @typescript-eslint/no-explicit-any
function process(data: any) { }         // Error: Unexpected any
function process(data: unknown) { }     // OK

// @typescript-eslint/no-unused-vars
const unused = 42;                      // Error: 'unused' is declared but never used
const _ignored = 42;                    // OK: underscore prefix ignored

// @typescript-eslint/no-floating-promises
fetchData();                            // Error: Promises must be awaited or returned
await fetchData();                      // OK
void fetchData();                       // OK (explicitly ignoring)

// @typescript-eslint/consistent-type-imports
import { User } from "./types";         // Error: use 'import type' for type imports
import type { User } from "./types";    // OK

4. Prettier for formatting

Prettier is an opinionated code formatter. It handles how code looks — indentation, quotes, semicolons, line length:

npm install -D prettier

Create .prettierrc:

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 100,
  "arrowParens": "always",
  "bracketSpacing": true,
  "endOfLine": "lf"
}

Create .prettierignore:

dist/
node_modules/
*.min.js
coverage/

Running Prettier

# Format all files
npx prettier --write src/

# Check formatting (CI — fails if unformatted)
npx prettier --check src/

# Format a specific file
npx prettier --write src/app.ts
{
  "scripts": {
    "format": "prettier --write src/",
    "format:check": "prettier --check src/"
  }
}

5. Prettier vs ESLint — different responsibilities

ConcernESLintPrettier
PurposeCode quality and patternsCode formatting
CatchesBugs, anti-patterns, unused codeInconsistent whitespace, quotes, semicolons
ConfigurableHighly — hundreds of rulesMinimal — few options (opinionated)
Auto-fixSome rulesEverything (formatting is deterministic)
Examplesno-unused-vars, no-explicit-anyIndentation, line length, trailing commas

Mental model:

  • ESLint = "is my code correct and following best practices?"
  • Prettier = "is my code formatted consistently?"

6. Combining ESLint and Prettier

ESLint has some formatting rules that conflict with Prettier. Use eslint-config-prettier to disable ESLint's formatting rules:

npm install -D eslint-config-prettier

Flat config (ESLint 9+)

import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import prettierConfig from "eslint-config-prettier";

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  prettierConfig,  // Must be last — disables conflicting rules
);

Legacy config

{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ]
}

"prettier" must be last in the extends array — it overrides conflicting rules from earlier configs.


7. Editor integration

VS Code settings for auto-format and auto-lint

{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
    "source.organizeImports": "explicit"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}

What happens on save:

  1. Prettier formats the file
  2. ESLint auto-fixes what it can
  3. Imports are organized

This means you almost never need to manually run prettier or eslint --fix — it happens automatically.


8. Pre-commit hooks: husky + lint-staged

Enforce linting and formatting before code is committed:

# Install husky and lint-staged
npm install -D husky lint-staged

# Initialize husky
npx husky init

Create .husky/pre-commit:

npx lint-staged

Add to package.json:

{
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,css}": [
      "prettier --write"
    ]
  }
}

What happens on git commit:

  1. husky triggers the pre-commit hook
  2. lint-staged runs on only the staged files (not the entire project)
  3. ESLint fixes what it can
  4. Prettier formats the files
  5. If ESLint finds unfixable errors, the commit is blocked
  6. Fixed files are automatically re-staged

Benefits:

  • Bad code never enters the repository
  • Only staged files are checked (fast)
  • Auto-fixes are applied before commit

9. CI pipeline: typecheck + lint + format check

A complete CI configuration (GitHub Actions example):

# .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: 20
          cache: npm

      - run: npm ci

      - name: Type check
        run: npx tsc --noEmit

      - name: Lint
        run: npx eslint src/ --ext .ts,.tsx

      - name: Format check
        run: npx prettier --check src/

      - name: Test
        run: npm test

The pipeline script in package.json:

{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "lint": "eslint src/ --ext .ts,.tsx",
    "lint:fix": "eslint src/ --ext .ts,.tsx --fix",
    "format": "prettier --write src/",
    "format:check": "prettier --check src/",
    "test": "vitest run",
    "ci": "npm run typecheck && npm run lint && npm run format:check && npm run test"
  }
}

10. Real setup: complete ESLint + Prettier config

Here is a complete, production-ready setup:

# Install all dependencies
npm install -D \
  typescript \
  eslint \
  @typescript-eslint/parser \
  @typescript-eslint/eslint-plugin \
  prettier \
  eslint-config-prettier \
  husky \
  lint-staged

eslint.config.js (flat config):

import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import prettierConfig from "eslint-config-prettier";

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  prettierConfig,
  {
    rules: {
      "@typescript-eslint/no-explicit-any": "error",
      "@typescript-eslint/no-unused-vars": ["error", {
        argsIgnorePattern: "^_",
        varsIgnorePattern: "^_",
      }],
      "@typescript-eslint/consistent-type-imports": ["error", {
        prefer: "type-imports",
      }],
    },
  },
  {
    ignores: ["dist/", "node_modules/", "coverage/"],
  },
);

.prettierrc:

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

package.json scripts:

{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "typecheck": "tsc --noEmit",
    "lint": "eslint src/ --ext .ts,.tsx",
    "lint:fix": "eslint src/ --ext .ts,.tsx --fix",
    "format": "prettier --write src/",
    "format:check": "prettier --check src/",
    "test": "vitest run",
    "ci": "npm run typecheck && npm run lint && npm run format:check && npm run test"
  },
  "lint-staged": {
    "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{json,md,css}": ["prettier --write"]
  }
}

Key takeaways

  1. ESLint catches code quality issues that TypeScript's type system misses — use @typescript-eslint packages.
  2. Prettier enforces consistent formatting — keep it opinionated, do not fight it.
  3. Use eslint-config-prettier to prevent ESLint/Prettier conflicts.
  4. Format on save in VS Code eliminates manual formatting.
  5. husky + lint-staged enforce standards before code is committed.
  6. CI pipeline: typecheck + lint + format check + test + build — in that order.

Explain-It Challenge

Explain without notes:

  1. What is the difference between what ESLint catches and what TypeScript catches?
  2. Why do you need eslint-config-prettier when using both tools?
  3. How do husky and lint-staged work together to enforce code quality?

Navigation: ← 1.25 Overview · 1.25-Exercise-Questions →