9.5 Type Check Optimization — Faster TypeScript Builds
Why Type Checking Slows Down
If tsc --noEmit takes tens of seconds in a large TypeScript project, check these factors:
| Cause | Impact | Solution |
|---|---|---|
| Full recompilation | High | incremental builds |
| Library type checking | Medium | skipLibCheck |
| Complex type inference | High | Simplify types |
| Too many files included | Medium | Optimize include/exclude |
incremental Builds
The most effective optimization. Only recompiles changed files.
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo" // Cache file location
}
}
# First run: full compilation (slow)
tsc --noEmit
# .tsbuildinfo file is created
# Second run: only changed files compiled (fast)
tsc --noEmit
Add to .gitignore
# TypeScript build cache
*.tsbuildinfo
incremental Builds in CI
# .github/workflows/typecheck.yml
- name: Cache TypeScript build info
uses: actions/cache@v3
with:
path: .tsbuildinfo
key: tsbuildinfo-${{ hashFiles('**/*.ts', 'tsconfig.json') }}
- name: Type check
run: tsc --noEmit
skipLibCheck
Skips type checking of .d.ts files in node_modules.
{
"compilerOptions": {
"skipLibCheck": true
}
}
Effects:
- 20–50% faster build speed
- Ignores third-party library type errors
When to use:
✅ Recommended: Almost all projects (fix library type errors by updating @types)
❌ Not recommended: Library development (type accuracy important)
isolatedModules
Forces each file to be transpilable independently. Required when using esbuild or swc.
{
"compilerOptions": {
"isolatedModules": true
}
}
This option is for bundler compatibility rather than performance. It ensures compatibility with single-file transpilation tools.
Optimizing include/exclude
Set the compilation scope precisely.
{
"compilerOptions": { },
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts", // Exclude test files (use separate tsconfig)
"**/*.spec.ts",
"**/__tests__/**",
"scripts/**" // Exclude build scripts
]
}
Separate tsconfig for Tests
// tsconfig.test.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["vitest/globals", "@types/jest"]
},
"include": [
"src/**/*.ts",
"src/**/*.test.ts",
"tests/**/*.ts"
]
}
Understanding .tsbuildinfo
The incremental build cache file, introduced in TypeScript 4.0.
// .tsbuildinfo (summarized structure)
{
"program": {
"fileInfos": {
"src/index.ts": {
"version": "abc123", // File hash
"signature": "xyz789" // Type signature hash
}
},
"options": { },
"referencedMap": { },
"exportedModulesMap": { },
"semanticDiagnosticsPerFile": []
}
}
TypeScript references this file to determine which files have changed.
Simplifying Complex Types
The more complex the type inference, the longer compilation takes.
Add Explicit Return Types
// ❌ Complex inference (slow)
function processUsers(users: User[]) {
return users
.filter(u => u.active)
.map(u => ({ ...u, displayName: `${u.firstName} ${u.lastName}` }))
.reduce((acc, u) => ({ ...acc, [u.id]: u }), {});
}
// ✅ Explicit return type (faster)
function processUsers(users: User[]): Record<string, ActiveUser> {
return users
.filter(u => u.active)
.map(u => ({ ...u, displayName: `${u.firstName} ${u.lastName}` }))
.reduce<Record<string, ActiveUser>>((acc, u) => ({ ...acc, [u.id]: u }), {});
}
Cache Complex Conditional Types
// ❌ Recalculated every time (slow)
type ExtractReturnTypes<T extends Record<string, (...args: any[]) => any>> = {
[K in keyof T]: ReturnType<T[K]>;
};
// ✅ Cache with intermediate types
type Fn = (...args: any[]) => any;
type FnRecord = Record<string, Fn>;
type ExtractReturnTypes<T extends FnRecord> = {
[K in keyof T]: ReturnType<T[K]>;
};
TypeScript Performance Diagnostics
--diagnostics Flag
tsc --noEmit --diagnostics
Example output:
Files: 156
Lines of Library: 37956
Lines of Definitions: 11304
Lines of TypeScript: 8901
Lines of JavaScript: 0
Lines of JSON: 246
Lines of Other: 0
Identifiers: 70426
Symbols: 59387
Types: 9248
Instantiations: 12871
Memory used: 133286K
Assignability cache size: 9098
Identity cache size: 162
Subtype cache size: 57
Strict subtype cache size: 2499
I/O Read time: 0.13s
Parse time: 0.44s
ResolveModule time: 0.19s
ResolveLibrary time: 0.01s
ResolveTypeReference time: 0.00s
Bind time: 0.33s
Check time: 1.91s ← Pay attention here
Emit time: 0.00s
Total time: 3.02s
If Check time is high, simplify complex type inference.
--extendedDiagnostics Flag
Outputs more detailed diagnostic information.
tsc --noEmit --extendedDiagnostics 2>&1 | grep "Check time"
Real-World Optimization Configuration
Large Project tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
// Performance optimizations
"incremental": true,
"tsBuildInfoFile": ".cache/.tsbuildinfo",
"skipLibCheck": true,
"isolatedModules": true,
// Minimize scope
"noEmit": true
},
"include": ["src"],
"exclude": [
"node_modules",
"dist",
".cache",
"**/*.test.*",
"**/__mocks__/**"
]
}
Build Time Measurement Script
# macOS/Linux
time tsc --noEmit
# Repeated measurements
for i in 1 2 3; do time tsc --noEmit 2>&1; done
Parallel Type Checking
Type check multiple TypeScript projects in parallel.
# tsc --build builds in parallel when possible
tsc --build --verbose
Parallel Execution in GitHub Actions
jobs:
typecheck:
strategy:
matrix:
package: [web, api, mobile]
steps:
- run: tsc --noEmit
working-directory: apps/${{ matrix.package }}
Pro Tips
1. Separate type checking during development
// package.json
{
"scripts": {
"dev": "vite", // Bundling (no type check)
"typecheck": "tsc --noEmit", // Type check only
"typecheck:watch": "tsc --noEmit --watch" // Watch mode type check
}
}
2. Instant error detection with VSCode settings
// .vscode/settings.json
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
3. Type check skip patterns (emergency workarounds)
// @ts-ignore — Ignore error on next line (explain why!)
// @ts-ignore: Legacy API, type definition to be added later
legacyFunction(data);
// @ts-expect-error — Error is expected (reverse error if none)
// @ts-expect-error: Intentionally wrong type for testing
const result: string = 42;
// @ts-nocheck — Ignore type checking for entire file (temporary during migration)
// @ts-nocheck