Skip to main content
Advertisement

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:

CauseImpactSolution
Full recompilationHighincremental builds
Library type checkingMediumskipLibCheck
Complex type inferenceHighSimplify types
Too many files includedMediumOptimize 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
Advertisement