How to configure TypeScript for fast builds and responsive IDE feedback — incremental compilation, tsconfig tuning, decoupling type checking from transpilation, and project references for monorepos.
TypeScript's type safety is worth the tradeoff — until the build takes 3 minutes and the IDE takes 5 seconds to show an autocomplete suggestion.
The performance problems are predictable and fixable. This guide covers the specific configurations that make TypeScript fast at scale: incremental compilation, tsconfig tuning, separating type checking from transpilation, and project references for monorepos.
Three things cause TypeScript to slow down as codebases grow:
1. No incremental builds. By default, tsc re-analyses every file from scratch. At 500+ files, this adds up.
2. Checking node_modules type definitions. Your dependencies ship .d.ts files. Checking them adds significant time, and you didn't write them — there's nothing actionable you can do with those errors.
3. Type checking coupled to the build. When your bundler runs tsc as part of the build, every hot-reload waits for type checking to complete. These are different concerns and should run separately.
{
"compilerOptions": {
// Performance
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo",
"skipLibCheck": true,
// Correctness
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Compatibility
"moduleResolution": "bundler",
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"esModuleInterop": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}incremental: true — TypeScript saves a .tsbuildinfo file after each compilation. On subsequent builds, it reads this file and only rechecks files that have changed. First build is unchanged; subsequent builds are dramatically faster.
skipLibCheck: true — Skips type checking of .d.ts files in node_modules. This is almost always correct: you're not fixing library type errors, and checking them adds significant time for zero benefit.
noUncheckedIndexedAccess: true — Goes beyond strict mode. Array access (arr[0]) and object index access return T | undefined instead of T. This catches a class of runtime errors that strict misses.
This is the highest-leverage change for development speed. Most setups run type checking as part of the build, which means every file save blocks on the TypeScript compiler.
The correct approach:
Your bundler handles transpilation only. Tools like SWC and esbuild strip TypeScript types to produce JavaScript near-instantly — they don't check types at all. This is correct for development: you need runnable code, not verified code.
Type checking runs separately. Run tsc --noEmit --watch in a terminal alongside your dev server. Errors appear in that terminal. They don't block your build.
In a Next.js project:
# Terminal 1: dev server (fast, no type checking)
pnpm dev
# Terminal 2: type checker (runs in background, reports errors)
pnpm tsc --noEmit --watchOr as a package.json script:
{
"scripts": {
"type-check": "tsc --noEmit",
"type-check:watch": "tsc --noEmit --watch"
}
}In CI, run type-check as a separate job — in parallel with lint and tests, not blocking the build.
Add .vscode/settings.json to your project so these apply to everyone on the team:
{
"typescript.preferences.includePackageJsonAutoImports": "off",
"typescript.suggest.autoImports": true,
"typescript.updateImportsOnFileMove.enabled": "always",
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
}includePackageJsonAutoImports: "off" — Without this, VS Code scans every package.json in node_modules when generating auto-import suggestions. In a monorepo, this causes significant autocomplete lag. Turning it off has no practical downside — it just stops suggesting obscure internal package paths.
In a monorepo with multiple TypeScript packages, changing one package triggers a full recheck of all packages by default. Project references fix this.
Each package gets a tsconfig.json that declares its dependencies:
// packages/ui/tsconfig.json
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src"
},
"references": [
{ "path": "../utils" }
]
}The root tsconfig.json references all packages:
// tsconfig.json (root)
{
"files": [],
"references": [
{ "path": "./packages/utils" },
{ "path": "./packages/ui" },
{ "path": "./apps/web" }
]
}Build with tsc --build (or tsc -b) instead of tsc. TypeScript now understands the dependency graph and only rebuilds packages affected by your change.
composite: true is required on referenced packages — it tells TypeScript that this package will be referenced by others, and enables the incremental outputs that make this work.
Before making changes, baseline your current build time:
time tsc --noEmitAfter enabling incremental builds, run it twice — the second run shows the cached improvement:
time tsc --noEmit # first run (cold)
time tsc --noEmit # second run (should be 60-80% faster)For a 500-file codebase, expect cold build around 15–30 seconds, incremental build 3–8 seconds. At 2,000 files, incremental builds stay fast while cold builds scale linearly.
Running tsc in your webpack/vite config. ts-loader with transpileOnly: false runs full type checking on every file save. Switch to transpileOnly: true or replace with swc-loader entirely.
Not committing .tsbuildinfo. If CI deletes node_modules and .tsbuildinfo between runs, incremental builds don't help in CI. Cache .tsbuildinfo along with node_modules.
Circular dependencies. TypeScript handles circular imports differently from bundlers and they silently degrade type inference quality. Use madge --circular . to find them.
A slow TypeScript setup is usually a configuration problem, not a scale problem. These changes — incremental builds, skipLibCheck, decoupled type checking, and project references — are what we implement as part of every DX engagement.
If your team is spending meaningful time waiting on TypeScript, that's engineering time that should be building product.
→ Developer Experience Consultancy
Call +91 90358 61690 · Book a free call · Contact form
If this guide resonated with your situation, let's talk. We offer a free 30-minute discovery call — no pitch, just honest advice on your specific project.
How to set up Drizzle ORM with PostgreSQL from scratch — schema definition, migrations, query patterns, connection pooling, and the configuration decisions that matter in production Next.js applications.
11 min readguideA technical guide to database indexes: B-tree internals, composite index column ordering, covering indexes, partial indexes, the write cost of over-indexing, EXPLAIN ANALYZE interpretation, and the common indexing mistakes that degrade production performance.
14 min read