Step 1: Understanding the Error
When migrating to a monorepo structure, you'll likely encounter errors like these during the build phase:
error TS5055: Root file specified for compilation that resolves to an output file.
error: Could not resolve "node" module (did you forget to install it?)
error TS2688: Cannot find type definition file for 'node'.
Did you forget to install '@types/node'?
error: Failed to transpile dependencies: stripping node types stripped away critical type information
These errors appear when TypeScript tries to compile your packages but can't properly resolve or strip Node.js type definitions. The issue becomes more pronounced in monorepos because each workspace has its own build context and dependency resolution rules.
Step 2: Identifying the Cause
The root problem stems from how TypeScript handles type stripping across package boundaries in monorepos. Here are the main culprits:
Incorrect tsconfig inheritance: Child packages inherit parent settings that don't account for individual workspace requirements. When the parent's tsconfig strips types too aggressively or has incompatible module resolution, child packages break during compilation.
Missing or conflicting @types/node: Monorepo root might have @types/node installed, but child packages can't access it due to resolution configuration. This creates gaps where Node.js types (like Buffer, Stream, process) vanish during the build.
Module resolution mismatch: Different packages use different module resolution strategies (node, bundler, classic). When these conflict, TypeScript can't properly resolve type definitions from node_modules.
Incorrect isolatedModules setting: Set to true in the root tsconfig, this forces each file to be transpiled independently without type context. This strips type information that's needed for cross-package references.
Let's look at a broken monorepo setup:
monorepo-root/
├── packages/
│ ├── shared/
│ │ ├── tsconfig.json
│ │ └── src/
│ │ └── utils.ts
│ ├── api/
│ │ ├── tsconfig.json
│ │ └── src/
│ │ └── server.ts
│ └── cli/
│ ├── tsconfig.json
│ └── src/
│ └── index.ts
├── tsconfig.json
└── package.json
Step 3: Implementing the Solution
Solution A: Fix Root tsconfig.json
Start by correcting the root configuration. This is the foundation that all child packages reference.
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": false,
"lib": ["ES2020"],
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
Key changes here:
isolatedModules: falseallows TypeScript to use full type context across filesmoduleResolution: "node"enables proper Node.js resolutiontypes: ["node"]explicitly includes Node typesskipLibCheck: truespeeds up compilation by skipping type checking of declaration files
Solution B: Configure Child Package tsconfigs
Each workspace package should extend the root configuration while maintaining workspace-specific settings.
packages/shared/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"declaration": true,
"declarationDir": "./dist/types"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
packages/api/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"types": ["node", "jest"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
packages/cli/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"module": "commonjs",
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
Solution C: Install Dependencies Correctly
Ensure @types/node is available at the root level:
$ npm install -D @types/node
# For yarn workspaces
$ yarn add -W -D @types/node
# For pnpm
$ pnpm add -D @types/node
Verify installation:
$ ls node_modules/@types/node
# Output should show version folder (e.g., v20.9.0)
# containing package.json, index.d.ts, etc.
Solution D: Update package.json Build Scripts
Configure consistent build commands across packages:
packages/shared/package.json
{
"name": "@monorepo/shared",
"version": "1.0.0",
"scripts": {
"build": "tsc --project tsconfig.json",
"build:watch": "tsc --project tsconfig.json --watch"
},
"devDependencies": {
"typescript": "^5.2.0"
}
}
packages/api/package.json
{
"name": "@monorepo/api",
"version": "1.0.0",
"dependencies": {
"@monorepo/shared": "workspace:*"
},
"scripts": {
"build": "tsc --project tsconfig.json",
"build:watch": "tsc --project tsconfig.json --watch"
},
"devDependencies": {
"typescript": "^5.2.0",
"@types/node": "^20.0.0"
}
}
Step 4: Working Code Example
Here's a complete working example showing proper type usage across packages:
packages/shared/src/utils.ts
import { Buffer } from 'node:buffer';
import * as fs from 'node:fs/promises';
// Types are properly preserved when imported
export interface FileConfig {
path: string;
encoding: BufferEncoding;
}
export async function readConfig(filePath: string): Promise<FileConfig> {
// Node types (fs, Buffer) are correctly resolved
const content = await fs.readFile(filePath, 'utf-8');
return JSON.parse(content);
}
export function encodeBuffer(data: string): Buffer {
// Buffer type is preserved, not stripped
return Buffer.from(data, 'utf-8');
}
packages/api/src/server.ts
import { createServer, IncomingMessage, ServerResponse } from 'node:http';
import { readConfig, encodeBuffer } from '@monorepo/shared';
// Types from cross-package imports work correctly
async function startServer() {
const config = await readConfig('./config.json');
const server = createServer(
async (req: IncomingMessage, res: ServerResponse) => {
const data = encodeBuffer('Hello World');
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(data);
}
);
server.listen(3000, () => {
console.log('Server running on port 3000');
});
}
startServer().catch(console.error);
packages/cli/src/index.ts
import { process } from 'node:process';
import { readConfig } from '@monorepo/shared';
async function main() {
try {
// Node process type is available
const configPath = process.argv[2] || './config.json';
const config = await readConfig(configPath);
console.log('Config loaded:', config);
process.exit(0);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
}
main();
Build each package:
$ cd packages/shared && npm run build
$ cd ../api && npm run build
$ cd ../cli && npm run build
# No errors. Types are preserved across boundaries.
Additional Tips & Related Errors
Windows users: File path resolution may differ. Use forward slashes in tsconfig paths or use the path module for dynamic paths.
tsc vs build tools: If using esbuild or swc, ensure they have corresponding TypeScript plugins. esbuild doesn't perform full type checking—use tsc in your CI pipeline.
Circular dependencies: Monorepos often develop these. Check for imports where package-a imports from package-b which imports from package-a. Resolve by extracting common code to a third package.
Missing declaration files: After build, verify dist/types folders exist with proper .d.ts files:
$ find packages -name "*.d.ts" -type f
# Should list all generated type definition files
Performance: Large monorepos compile slowly with isolatedModules: false. For CI environments, consider splitting builds or using build cache tools like turbo:
$ npm install -D turbo
$ npx turbo build
# Builds only changed packages, reducing compilation time significantly
Related error—"Cannot find module '@monorepo/shared'": This appears when package references aren't in root package.json workspaces array:
{
"workspaces": [
"packages/shared",
"packages/api",
"packages/cli"
]
}
Related error—"Module has no exported member": Usually caused by build order issues. Ensure shared packages build before dependent packages. Use turbo's task dependencies:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
}
}
}
Clean rebuild if strange type errors persist:
$ rm -rf packages/*/dist node_modules
$ npm install
$ npm run build
The node type stripping issue typically resolves once you align tsconfig inheritance, explicitly include Node types, and ensure proper dependency installation across all workspace packages.