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 catches | ESLint catches |
|---|---|
| Wrong type assignments | Unused variables (with auto-fix) |
| Missing properties | Inconsistent patterns |
| Incorrect function arguments | Potential bugs (no-fallthrough in switch) |
| Null/undefined access | Code 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
| Rule | What it does | Recommendation |
|---|---|---|
@typescript-eslint/no-explicit-any | Warns on any usage | Error — forces proper typing |
@typescript-eslint/no-unused-vars | Warns on unused variables | Error — catches dead code |
@typescript-eslint/no-non-null-assertion | Warns on ! operator | Warn — prefer proper null checks |
@typescript-eslint/explicit-function-return-type | Requires return type annotations | Off or warn — inference is often enough |
@typescript-eslint/strict-boolean-expressions | Disallows truthy/falsy checks on non-booleans | Warn — catches subtle bugs |
@typescript-eslint/no-floating-promises | Requires handling of Promises | Error — catches unhandled async |
@typescript-eslint/no-misused-promises | Catches Promises used incorrectly | Error — prevents async bugs |
@typescript-eslint/prefer-nullish-coalescing | Suggests ?? over || for nullish values | Warn — more correct |
@typescript-eslint/prefer-optional-chain | Suggests ?. over manual checks | Warn — cleaner code |
@typescript-eslint/consistent-type-imports | Enforces import type for type-only imports | Error — 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
| Concern | ESLint | Prettier |
|---|---|---|
| Purpose | Code quality and patterns | Code formatting |
| Catches | Bugs, anti-patterns, unused code | Inconsistent whitespace, quotes, semicolons |
| Configurable | Highly — hundreds of rules | Minimal — few options (opinionated) |
| Auto-fix | Some rules | Everything (formatting is deterministic) |
| Examples | no-unused-vars, no-explicit-any | Indentation, 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:
- Prettier formats the file
- ESLint auto-fixes what it can
- 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:
- husky triggers the pre-commit hook
- lint-staged runs on only the staged files (not the entire project)
- ESLint fixes what it can
- Prettier formats the files
- If ESLint finds unfixable errors, the commit is blocked
- 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
- ESLint catches code quality issues that TypeScript's type system misses — use
@typescript-eslintpackages. - Prettier enforces consistent formatting — keep it opinionated, do not fight it.
- Use
eslint-config-prettierto prevent ESLint/Prettier conflicts. - Format on save in VS Code eliminates manual formatting.
- husky + lint-staged enforce standards before code is committed.
- CI pipeline: typecheck + lint + format check + test + build — in that order.
Explain-It Challenge
Explain without notes:
- What is the difference between what ESLint catches and what TypeScript catches?
- Why do you need
eslint-config-prettierwhen using both tools? - How do husky and lint-staged work together to enforce code quality?
Navigation: ← 1.25 Overview · 1.25-Exercise-Questions →