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/…"]
}
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.
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.
| Concern | Example rule | Why it helps |
| Unawaited async work | @typescript-eslint/no-floating-promises | Prevents silent failures and lost errors |
| Type only imports | @typescript-eslint/consistent-type-imports | Improves tree shaking and intent |
| Dead code | @typescript-eslint/no-unused-vars | Keeps surfaces small and clear |
| Any leakage | @typescript-eslint/no-explicit-any as warn | Highlights 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"
}
}
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 ."
}
}
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];
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}"
}
]
}
dist and your .tsbuildinfo file, then rebuild. Old maps cause confusing breakpoint behavior.