Introduction

TypeScript compilation failed out of memory errors occur when the TypeScript compiler (tsc) exhausts available heap memory during type checking, declaration emission, or JavaScript output generation. This critical build failure prevents application compilation, CI/CD pipeline completion, and deployment. The TypeScript compiler loads entire project graphs into memory, building Abstract Syntax Trees (ASTs), Type Objects, Symbol Tables, and Control Flow Graphs for type inference. Memory exhaustion happens when projects exceed Node.js heap limits (default ~2GB on 64-bit systems) due to large codebases, circular dependencies, complex generic types, deep type inference chains, or inefficient tsconfig configuration. Common causes include monorepo projects compiled as single unit, missing exclude patterns loading node_modules, path aliases creating complex resolution graphs, circular imports preventing tree shaking, overly broad include globs, triple-slash references loading unintended files, declaration file generation for large dependencies, watch mode accumulating type information, and TypeScript version inefficiencies. The fix requires understanding Node.js memory management, TypeScript compiler architecture, project reference patterns, incremental compilation, and tsconfig optimization strategies. This guide provides production-proven debugging patterns for TypeScript projects ranging from small applications to enterprise monorepos.

Symptoms

  • FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
  • TypeScript compilation exited with code 134 (SIGABRT - out of memory)
  • Build process killed by OOM killer on Linux
  • tsc process memory grows continuously until crash
  • Watch mode eventually crashes after multiple recompilations
  • CI/CD pipeline fails intermittently on TypeScript step
  • Build succeeds on local machine but fails in CI (lower memory limits)
  • TypeScript language server crashes in IDE
  • JavaScript heap out of memory in VSCode Output panel
  • Incremental builds slow down progressively until restart

Common Causes

  • Node.js heap limit too low for project size
  • Monorepo compiled as single TypeScript project
  • Missing exclude patterns loading node_modules
  • Circular dependencies preventing garbage collection
  • Complex generic type inference chains
  • Overly broad include globs (src/**/*)
  • Declaration emit for large dependency tree
  • Watch mode memory accumulation
  • Project references not configured for incremental builds
  • Triple-slash references loading unintended files

Step-by-Step Fix

### 1. Diagnose TypeScript memory usage

Check current memory allocation:

```bash # Run TypeScript with increased verbosity npx tsc --diagnostics --extendedDiagnostics

# Output shows memory usage: # Files: 453 # Lines: 98234 # Identifiers: 234567 # Symbols: 156789 # Types: 89012 # Instantiations: 234567 # Memory used: 1234567K (1.2GB) # Assignability cache size: 12345 # Emit time: 2.34s # Total time: 12.45s

# Key metrics: # - Memory used: Total heap consumption # - Types: Number of unique types in program # - Instantiations: Generic type instantiations (high = complex generics) # - Symbols: Number of symbols in symbol table ```

Monitor process memory during build:

```bash # Linux - Monitor tsc memory in real-time ps aux | grep tsc watch -n 1 'ps -o pid,rss,vsz,comm -C tsc'

# RSS (Resident Set Size) shows actual memory used # VSZ (Virtual Memory Size) shows allocated virtual memory

# Using /proc for detailed memory info pidof tsc | head -1 | xargs -I{} cat /proc/{}/status | grep -E "VmSize|VmRSS|VmPeak"

# macOS - Use vm_stat and Activity Monitor ps aux | grep tsc # Then open Activity Monitor > Search "tsc"

# Windows - Task Manager or PowerShell Get-Process tsc | Select-Object CPU,WorkingSet,VirtualMemory ```

Profile heap usage with Node.js inspector:

```bash # Run tsc with Node.js inspector node --inspect --max-old-space-size=4096 ./node_modules/.bin/tsc

# Open Chrome DevTools at chrome://inspect # Attach to Node.js process # Take heap snapshot during compilation

# Or generate heap profile node --heap-prof --max-old-space-size=4096 ./node_modules/.bin/tsc

# Heap profile saved to .heapprofile file # Analyze with Chrome DevTools or 0x profiler

# Generate flame graph for CPU and memory npx 0x --node-options="--max-old-space-size=4096" ./node_modules/.bin/tsc ```

Identify memory-heavy files:

```bash # TypeScript 4.7+ supports per-file diagnostics npx tsc --diagnostics --generateTrace ./trace-output

# Analyze trace with TypeScript analyze-trace tool npx @typescript/analyze-trace ./trace-output

# Output shows which files consume most memory/time: # File: src/large-module/types.ts # - Parse time: 234ms # - Bind time: 567ms # - Check time: 1234ms # - Emit time: 345ms # - Memory peak: 456MB

# Or use ts-morph to analyze AST size import { Project } from 'ts-morph';

const project = new Project({ tsConfigFilePath: "tsconfig.json" }); const sourceFiles = project.getSourceFiles();

sourceFiles.forEach(file => { console.log(${file.getFilePath()}: ${file.getFullText().length} chars); }); ```

### 2. Increase Node.js heap limit

Set NODE_OPTIONS environment variable:

```bash # Linux/macOS - Temporary for single command export NODE_OPTIONS="--max-old-space-size=4096" npx tsc

# Or inline NODE_OPTIONS="--max-old-space-size=4096" npx tsc

# Linux/macOS - Permanent in shell config echo 'export NODE_OPTIONS="--max-old-space-size=4096"' >> ~/.bashrc source ~/.bashrc

# Windows PowerShell - Session only $env:NODE_OPTIONS="--max-old-space-size=4096" npx tsc

# Windows PowerShell - Permanent [System.Environment]::SetEnvironmentVariable( 'NODE_OPTIONS', '--max-old-space-size=4096', 'User' )

# Windows CMD setx NODE_OPTIONS "--max-old-space-size=4096"

# Value is in megabytes: # 4096 = 4GB # 8192 = 8GB # 16384 = 16GB

# Check current setting echo $NODE_OPTIONS # Linux/macOS echo %NODE_OPTIONS% # Windows CMD $env:NODE_OPTIONS # PowerShell ```

Configure in package.json scripts:

json { "scripts": { "build": "node --max-old-space-size=4096 ./node_modules/.bin/tsc", "build:watch": "node --max-old-space-size=4096 ./node_modules/.bin/tsc --watch", "type-check": "node --max-old-space-size=4096 ./node_modules/.bin/tsc --noEmit" } }

Configure in CI/CD pipelines:

```yaml # GitHub Actions jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Set memory limit run: echo "NODE_OPTIONS=--max-old-space-size=4096" >> $GITHUB_ENV - name: Install dependencies run: npm ci - name: Build run: npm run build

# GitLab CI variables: NODE_OPTIONS: "--max-old-space-size=4096"

build: script: - npm ci - npm run build

# Jenkins pipeline pipeline { agent any environment { NODE_OPTIONS = '--max-old-space-size=4096' } stages { stage('Build') { steps { sh 'npm run build' } } } }

# Dockerfile FROM node:20-alpine ENV NODE_OPTIONS="--max-old-space-size=4096" WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build ```

### 3. Optimize tsconfig.json

Add exclude patterns to reduce files:

json { "compilerOptions": { "target": "ES2020", "module": "ESNext", "lib": ["ES2020"], "skipLibCheck": true, "declaration": true, "outDir": "./dist" }, "include": [ "src/**/*" ], "exclude": [ "node_modules", "dist", "build", "coverage", "**/*.test.ts", "**/*.spec.ts", "**/__tests__/**", "**/__mocks__/**", "src/**/*.stories.tsx", "e2e", "playwright", "cypress" ] }

Key exclude considerations: - **node_modules**: Always exclude (use skipLibCheck instead) - **Test files**: Exclude from production build, run separately - **Build output**: Prevent recursive compilation - **Storybook/Cypress**: Separate compilation contexts - **Large generated files**: Exclude auto-generated code

Configure skipLibCheck and skipDefaultLibCheck:

json { "compilerOptions": { "skipLibCheck": true, "skipDefaultLibCheck": true } }

Effects: - skipLibCheck: true: Skip type checking of .d.ts files (saves memory) - skipDefaultLibCheck: true: Skip type checking of default library files - Safe to enable for most projects - declaration files are pre-compiled - Can save 20-30% memory on large projects

Disable unnecessary features:

```json { "compilerOptions": { // Disable if not using decorators "experimentalDecorators": false,

// Disable if not using emit decorators "emitDecoratorMetadata": false,

// Disable source maps for release builds "sourceMap": false, "inlineSourceMap": false,

// Disable inline sources "inlineSources": false,

// Disable if not generating declarations "declaration": false, "declarationMap": false,

// Disable if not using composite builds "composite": false, "incremental": false } } ```

### 4. Implement project references

Split monorepo into project references:

```json // tsconfig.json (root) { "files": [], "references": [ { "path": "./packages/core" }, { "path": "./packages/utils" }, { "path": "./packages/api" }, { "path": "./packages/web" } ] }

// packages/core/tsconfig.json { "compilerOptions": { "composite": true, "declaration": true, "declarationMap": true, "incremental": true, "tsBuildInfoFile": "./dist/tsBuildInfo.json", "outDir": "./dist" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] }

// packages/api/tsconfig.json { "compilerOptions": { "composite": true, "declaration": true, "incremental": true, "tsBuildInfoFile": "./dist/tsBuildInfo.json" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"], "references": [ { "path": "../core" }, { "path": "../utils" } ] } ```

Build with project references:

```bash # Build all projects npx tsc --build

# Build specific project npx tsc --build packages/core

# Build with verbose output npx tsc --build --verbose

# Force rebuild (ignore incremental cache) npx tsc --build --force

# Clean build artifacts npx tsc --build --clean

# Watch mode with project references npx tsc --build --watch

# Key benefits: # - Each project compiled separately (lower memory per project) # - Incremental compilation (only rebuild changed projects) # - Parallel compilation possible # - tsBuildInfo.json caches type information ```

### 5. Enable incremental compilation

Configure incremental builds:

json { "compilerOptions": { "incremental": true, "tsBuildInfoFile": "./node_modules/.cache/tsbuildinfo.json" } }

  1. How incremental compilation works:
  2. First build: Full compilation, saves tsBuildInfo.json
  3. Subsequent builds: Read previous state, recompile only changed files
  4. Memory savings: Only load changed files and dependencies into memory
  5. Build info stored separately from output (can use different location)

Combine with project references for maximum benefit:

json { "compilerOptions": { "composite": true, "incremental": true, "declaration": true, "declarationMap": true, "sourceMap": true, "tsBuildInfoFile": "./dist/tsbuildinfo.json" } }

Use build mode instead of regular compilation:

```bash # Regular mode - loads all files every time npx tsc

# Build mode - uses incremental cache npx tsc --build

# Build mode is 5-10x faster for large projects # And uses significantly less memory ```

### 6. Fix circular dependencies

Detect circular dependencies:

```bash # Install madge for dependency analysis npm install --save-dev madge

# Generate dependency graph npx madge --circular --extensions ts src/

# Output shows circular imports: # Found 3 circular dependencies! # 1) src/services/auth.ts > src/services/user.ts > src/services/auth.ts # 2) src/components/A.tsx > src/components/B.tsx > src/components/A.tsx # 3) ...

# Generate visual graph npx madge --image graph.svg --extensions ts src/

# Or with TypeScript project references npx madge --circular --project tsconfig.json ```

Break circular dependencies:

```typescript // BEFORE: Circular dependency // file-a.ts import { FileB } from './file-b'; export class FileA { useB(b: FileB) { } }

// file-b.ts import { FileA } from './file-a'; export class FileB { useA(a: FileA) { } }

// AFTER: Extract shared interface // file-a.ts import { IFileB } from './interfaces'; export class FileA { useB(b: IFileB) { } }

// file-b.ts import { FileA } from './file-a'; import { IFileB } from './interfaces'; export class FileB implements IFileB { useA(a: FileA) { } }

// interfaces.ts // Shared types extracted to break cycle export interface IFileB { useA(a: any): void; } ```

Use dependency injection:

```typescript // BEFORE: Direct import creates cycle // order.service.ts import { UserService } from './user.service'; export class OrderService { private userService = new UserService(); }

// user.service.ts import { OrderService } from './order.service'; export class UserService { private orderService = new OrderService(); }

// AFTER: Dependency injection breaks cycle // interfaces.ts export interface IUserService { getUser(id: string): Promise<User>; }

export interface IOrderService { getOrders(userId: string): Promise<Order[]>; }

// user.service.ts import { IOrderService } from './interfaces'; export class UserService implements IUserService { constructor(private orderService: IOrderService) {} getUser(id: string) { } }

// order.service.ts import { IUserService } from './interfaces'; export class OrderService implements IOrderService { constructor(private userService: IUserService) {} getOrders(userId: string) { } }

// Container wires dependencies const orderService = new OrderService(userService); const userService = new UserService(orderService); ```

### 7. Optimize path aliases and module resolution

Simplify path aliases:

```json // tsconfig.json { "compilerOptions": { "baseUrl": "./src", "paths": { // BAD: Too broad, matches everything "@/*": ["*"],

// GOOD: Specific paths "@components/*": ["components/*"], "@services/*": ["services/*"], "@hooks/*": ["hooks/*"], "@utils/*": ["utils/*"], "@types/*": ["types/*"] } } } ```

Configure module resolution:

```json { "compilerOptions": { // Use Node.js resolution (faster than classic) "moduleResolution": "Node",

// Skip checking library files "skipLibCheck": true,

// Isolate modules for faster compilation "isolatedModules": true,

// Resolve JSON modules if needed "resolveJsonModule": true } } ```

Remove unused path aliases:

```bash # Find unused aliases in codebase grep -r "@components/" src/ | wc -l grep -r "@services/" src/ | wc -l

# Or use TypeScript's --listFiles to see what's being included npx tsc --listFiles | grep -E "@components|@services"

# Remove aliases that aren't used ```

### 8. Reduce generic type complexity

Simplify complex generics:

```typescript // BEFORE: Deeply nested generics cause explosion type ComplexType<T> = { data: T; nested: { items: Array<{ value: T; children: Array<{ result: Promise<Wrapper<T>>; }>; }>; }; };

// Type instantiation too deep error // Causes excessive memory during type checking

// AFTER: Flatten with intermediate types type ItemValue<T> = { value: T }; type ItemChild<T> = { result: Promise<Wrapper<T>> }; type Item<T> = { value: T; children: ItemChild<T>[] }; type Nested<T> = { items: Item<T>[] }; type ComplexType<T> = { data: T; nested: Nested<T> }; ```

Use type annotations instead of inference:

```typescript // BEFORE: Let TypeScript infer everything (memory intensive) const result = data .filter(x => x.active) .map(x => transform(x)) .reduce((acc, item) => process(acc, item), initialValue);

// TypeScript must infer type for each step // Creates many intermediate type objects

// AFTER: Annotate to reduce inference work const filtered: Item[] = data.filter(x => x.active); const mapped: Transformed[] = filtered.map(x => transform(x)); const result: FinalType = mapped.reduce( (acc: FinalType, item: Transformed) => process(acc, item), initialValue ); ```

Limit conditional type recursion:

```typescript // BEFORE: Recursive conditional types type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]; };

// Can cause infinite recursion on complex types

// AFTER: Limit depth or use utility type DeepPartial<T, Depth extends number = 3> = Depth extends 0 ? T : { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K], Prev<Depth>> : T[K] };

type Prev<N extends number> = N extends 0 ? 0 : [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20][N]; ```

### 9. Exclude declaration files and tests

Separate test and build tsconfig:

```json // tsconfig.json (base) { "compilerOptions": { "target": "ES2020", "module": "ESNext", "skipLibCheck": true, "outDir": "./dist" }, "exclude": ["node_modules", "dist"] }

// tsconfig.build.json (production build) { "extends": "./tsconfig.json", "include": ["src/**/*"], "exclude": [ "node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/__tests__/**", "**/__mocks__/**" ] }

// tsconfig.test.json (testing) { "extends": "./tsconfig.json", "include": [ "src/**/*", "**/*.test.ts", "**/*.spec.ts" ], "compilerOptions": { "types": ["jest", "node"] } } ```

Build commands:

```bash # Production build (excludes tests) npx tsc --project tsconfig.build.json

# Test type-checking only npx tsc --project tsconfig.test.json --noEmit

# In CI/CD npm run build:prod # Uses tsconfig.build.json npm run test:type-check # Uses tsconfig.test.json ```

Exclude generated files:

json { "compilerOptions": {}, "exclude": [ "node_modules", "dist", "src/generated/**/*", "src/graphql/generated/**/*", "**/*.codegen.ts", "openapi-types.ts", "schema.d.ts" ] }

Generated files to consider excluding: - GraphQL generated types - OpenAPI/Swagger generated types - Protocol Buffers generated files - Prisma generated client types - Custom codegen output

### 10. Use alternative build tools

Switch to esbuild for faster builds:

```bash # Install esbuild npm install --save-dev esbuild

# Use esbuild for transpilation only (no type checking) npx esbuild src/**/*.ts --outdir=dist --platform=node --format=cjs

# Or with tsup (esbuild wrapper) npm install --save-dev tsup npx tsup src/index.ts --format cjs --dts ```

Use swc for compilation:

```bash # Install swc npm install --save-dev @swc/cli @swc/core

# Compile with swc npx swc src/ --out-dir dist --strip-leading-paths

# Watch mode npx swc src/ --out-dir dist --watch ```

Split type checking and bundling:

json { "scripts": { "type-check": "tsc --noEmit", "build": "esbuild src/index.ts --bundle --outfile=dist/bundle.js", "build:types": "tsc --emitDeclarationOnly --declaration --outDir dist/types" } }

Use Vite for development:

```bash # Vite uses esbuild for TypeScript (10-100x faster than tsc) npm install vite

# vite.config.ts export default { build: { target: 'es2020', minify: 'esbuild', sourcemap: true } } ```

Prevention

  • Use project references for monorepos larger than 10 packages
  • Enable incremental compilation for all projects
  • Set NODE_OPTIONS in CI/CD environment variables
  • Exclude test files from production tsconfig
  • Run type checking separately from bundling
  • Monitor build memory usage in CI alerts
  • Break circular dependencies when detected
  • Use skipLibCheck by default for application projects
  • **JavaScript heap out of memory**: Node.js memory exhausted
  • **Type instantiation too deep**: Recursive generic types
  • **Maximum call stack size exceeded**: Infinite type recursion
  • **ENOENT: no such file or directory**: Path resolution failure
  • **TS2307 Cannot find module**: Module resolution failure