<< Home

Chapter 8: Tooling, Linting, and Formatting

Great tooling makes TypeScript projects feel responsive during development and predictable in production. This chapter focuses on the TypeScript compiler and build strategies, linting with ESLint, formatting with Prettier, and creating reliable debugging experiences with source maps. The goal is a feedback loop that is fast, consistent, and clear.

tsc and project references

The TypeScript compiler tsc is more than a transpiler. It can type check, emit JavaScript, create incremental builds, and orchestrate multi package repositories. Thoughtful configuration in tsconfig.json gives major speed improvements and clearer boundaries between packages.

Core tsconfig.json options

Start with sensible compiler options. The combination of incremental and composite enables caching and project references. Use skipLibCheck to reduce work during inner loop development, then re enable for release builds if your quality bar requires it.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "dist",
    "rootDir": "src",
    "declaration": true,
    "sourceMap": true,
    "inlineSources": true,
    "strict": true,
    "incremental": true,
    "composite": true,
    "skipLibCheck": true,
    "tsBuildInfoFile": ".tsbuildinfo",
    "isolatedModules": true
  },
  "include": ["src/…"]
}
💡 Ensure emphasis on the inner loop by keeping skipLibCheck true and running a separate full type check in CI so daily edits stay quick, while releases still receive thorough verification.

Multi package repos

Project references let one package build only after its dependencies have compiled. Each package becomes a standalone project with an output directory and declarations. The repository root then drives a coordinated build using the -b flag.

// ./packages/util/tsconfig.json
{
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src",
    "declaration": true,
    "composite": true
  },
  "include": ["src/…"]
}

// ./packages/app/tsconfig.json
{
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src",
    "composite": true
  },
  "references": [{ "path": "../util" }],
  "include": ["src/…"]
}

// ./tsconfig.json at repo root
{
  "files": [],
  "references": [
    { "path": "packages/util" },
    { "path": "packages/app" }
  ]
}

Build everything using a single command that respects reference order and caches results between runs.

npx tsc -b

Speed tactics that make a difference

Small adjustments compound to large gains. Target a modern ECMAScript level to reduce transform work. Avoid unnecessary type widening by keeping files focused. Prefer paths with baseUrl for clean imports and predictable cache keys. Keep your outDir clean to avoid stale artifacts.

⚠️ When switching target or module, clear old outputs. Stale JavaScript can mask problems and slow the next build while the compiler tries to reconcile mismatched artifacts.

ESLint recommended rules

ESLint enforces code quality and consistency that go beyond formatting. With the TypeScript parser, it can understand types to catch logic issues. Use a minimal core plus well chosen plugin rules so checks run fast while still guiding good practices.

Parser and plugin setup

Install the parser and the main plugin, then create a config file that points to your tsconfig.json. The parser uses your settings to understand path mapping and module resolution.

npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
// eslint.config.js (flat config)
import ts from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";

export default [
  {
    files: ["**/*.ts", "**/*.tsx"],
    languageOptions: {
      parser: tsParser,
      parserOptions: {
        project: "./tsconfig.json",
        tsconfigRootDir: __dirname
      }
    },
    plugins: { "@typescript-eslint": ts },
    rules: {
      "@typescript-eslint/consistent-type-imports": "warn",
      "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
      "@typescript-eslint/no-floating-promises": "error",
      "no-console": "off"
    }
  }
];

Recommended rule themes

Choose rules that prevent common pitfalls in TypeScript codebases. Focus on unused entities, unsafe promises, and clarity around imports. Prefer warning severity for stylistic nudges and error severity for correctness.

ConcernExample ruleWhy it helps
Unawaited async work@typescript-eslint/no-floating-promisesPrevents silent failures and lost errors
Type only imports@typescript-eslint/consistent-type-importsImproves tree shaking and intent
Dead code@typescript-eslint/no-unused-varsKeeps surfaces small and clear
Any leakage@typescript-eslint/no-explicit-any as warnHighlights escaping the type system

Running ESLint

Wire scripts so developers can lint changed files locally and CI can run the full set. Keep the command parameters simple and predictable so contributors do not have to remember flags.

// package.json
{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  }
}
💡 Use eslint --max-warnings 0 in CI when you want warning free merges. This makes the bar explicit and avoids slow back and forth about which violations are acceptable.

Prettier integration

Prettier removes style debates by formatting code to a consistent standard. Let Prettier own layout and whitespace while ESLint focuses on logic quality. Avoid overlapping responsibilities to keep feedback clear.

Install and configure

Install the formatter and a small config. Keep options minimal. The fewer toggles you use, the easier it is for teams to accept automatic formatting everywhere.

npm i -D prettier eslint-config-prettier
// .prettierrc.json
{
  "printWidth": 100,
  "singleQuote": true,
  "trailingComma": "all"
}
// eslint.config.js (add last)
import prettier from "eslint-config-prettier";
export default [
  /* … existing entries … */,
  prettier
];

Editor and script workflow

Enable format on save in your editor and expose a project script so contributors can format on demand. This ensures consistent output across platforms and avoids manual adjustments.

// package.json
{
  "scripts": {
    "format": "prettier --write .",
    "format:check": "prettier --check ."
  }
}
⚠️ Do not enable rules that conflict with Prettier formatting. Keep eslint-config-prettier as the final extension in your config so formatting related lint rules are disabled.

Ignoring files and regions

Sometimes generated files or embedded content should not be reformatted. Use ignore files and inline directives sparingly so exceptions remain rare and obvious.

// .prettierignore
dist
coverage
*.min.js
// Inline ignore next line
// prettier-ignore
const layout = [1,  2,   3];
💡 Run prettier --cache for large repositories. The cache speeds up repeated runs by skipping unchanged files.

Source maps and debugging

Source maps link emitted JavaScript back to your original TypeScript so stack traces and breakpoints make sense. Configure maps in the compiler, ensure Node understands them, and set up editor launch settings for a smooth workflow.

Compiler settings that help stack traces

Enable sourceMap and inlineSources so debuggers can display the original files. Keep outputs and sources side by side using outDir so paths stay predictable.

{
  "compilerOptions": {
    "outDir": "dist",
    "sourceMap": true,
    "inlineSources": true
  }
}

Running Node with source map support

Modern Node reads source maps from //# sourceMappingURL comments. Use the --enable-source-maps flag to improve stack traces from unhandled errors and promise rejections.

node --enable-source-maps dist/index.js

During development, many teams use tsx or ts-node to run TypeScript directly. Both tools produce stack traces that map to .ts files when configured with source maps.

npm i -D tsx
// package.json
{
  "scripts": {
    "dev": "tsx watch src/index.ts"
  }
}

Debugging in VS Code

Create a launch configuration that points to the built file or uses a runtime that understands TypeScript. Keep the mapping simple so breakpoints bind reliably with or without a watch task.

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Run built app",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/dist/index.js",
      "cwd": "${workspaceFolder}",
      "runtimeArgs": ["--enable-source-maps"]
    },
    {
      "name": "Run via tsx",
      "type": "node",
      "request": "launch",
      "runtimeExecutable": "tsx",
      "runtimeArgs": ["watch"],
      "program": "${workspaceFolder}/src/index.ts",
      "cwd": "${workspaceFolder}"
    }
  ]
}
💡 When stepping jumps unexpectedly, delete dist and your .tsbuildinfo file, then rebuild. Old maps cause confusing breakpoint behavior.