Solving TypeScript Path Mapping Issues in Monorepos: Complete Guide
🔍 Understanding the Problem
In monorepo environments, TypeScript path mapping issues commonly manifest as:
Error: Cannot find module '@acme/ui-components' or its corresponding type declarations.
Error: Cannot find module '@acme/utils' or its corresponding type declarations.
These errors occur despite the modules existing because of mismatches between:
- TypeScript's module resolution (based on tsconfig paths)
- Build tools' module resolution (Webpack, Vite, Rollup, esbuild aliases)
- Node.js module resolution (when running dist/output directly)
- Package.json "exports" fields (modern package resolution)
📊 Monorepo Structure Example
my-monorepo/
├── packages/
│ ├── ui-components/ # @acme/ui-components
│ │ ├── src/
│ │ │ └── components/
│ │ ├── tsconfig.json
│ │ └── package.json
│ ├── utils/ # @acme/utils
│ │ ├── src/
│ │ │ └── helpers/
│ │ ├── tsconfig.json
│ │ └── package.json
│ └── api/ # @acme/api
│ ├── src/
│ │ │ └── routes/
│ │ ├── tsconfig.json
│ │ └── package.json
├── apps/
│ ├── web/ # Next.js/React app
│ │ ├── src/
│ │ │ └── components/
│ │ ├── tsconfig.json
│ │ └── package.json
│ └── docs/ # Documentation site
├── tsconfig.base.json
└── package.json
🚨 Common Path Mapping Issues in Monorepos
1. Tsconfig Paths Not Respected by Build Tools
// tsconfig.base.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@acme/*": ["packages/*/src"],
"@acme/ui-components": ["packages/ui-components/src"],
"@acme/utils": ["packages/utils/src"]
}
}
}
Problem: TypeScript understands @acme/ui-components, but Webpack/Vite/etc. don't → "Cannot find module" during build.
2. Package.json "exports" Conflicts
// packages/ui-components/package.json
{
"name": "@acme/ui-components",
"version": "1.0.0",
"exports": {
".": "./src/index.ts",
"./components/*": "./src/components/*.ts"
}
}
Problem: When consuming the package, Node.js uses exports field, but TypeScript might look at src/ directly, causing resolution mismatches.
3. Duplicate Package Issues
# Multiple versions of same package in node_modules
packages/ui-components/node_modules/react@18.2.0
packages/utils/node_modules/react@18.1.0
apps/web/node_modules/react@18.2.0
Problem: TypeScript sees different React versions → "Cannot assign types between versions" errors.
4. Path Mapping vs. Module Augmentation Conflicts
// In @acme/utils
declare module '@acme/ui-components' {
export interface ButtonProps {
// ... custom props
}
}
Problem: Path mapping makes module resolution unclear for augmentation.
5. Relative Path Leaks in Consuming Code
// ❌ PROBLEMATIC: Using relative paths breaks encapsulation
import { Button } from '../../../packages/ui-components/src/components/Button';
// Should be:
import { Button } from '@acme/ui-components';
🛠️ Solutions for Different Build Tools
Solution 1: Webpack Configuration
// webpack.config.js
const path = require('path');
module.exports = {
// ... other config
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
alias: {
// Match tsconfig paths exactly
'@acme/ui-components': path.resolve(__dirname, 'packages/ui-components/src'),
'@acme/utils': path.resolve(__dirname, 'packages/utils/src'),
'@acme/api': path.resolve(__dirname, 'packages/api/src'),
// Or use the base pattern
'@acme/(.*)': path.resolve(__dirname, 'packages/$1/src'),
},
// Important: symlinks for monorepo packages
symlinks: false,
// Prefer js/jsx over ts/tsx for compatibility
preferAbsolute: true,
modules: ['node_modules', path.resolve(__dirname, 'packages')],
},
plugins: [
// Help with path mapping warnings
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
})
]
};
Solution 2: Vite Configuration
// vite.config.js
import { defineConfig } from 'vite';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
// Match tsconfig paths
'@acme/ui-components': path.resolve(__dirname, 'packages/ui-components/src'),
'@acme/utils': path.resolve(__dirname, 'packages/utils/src'),
'@acme/api': path.resolve(__dirname, 'packages/api/src'),
// Using patterns
'@acme/(.*)': path.resolve(__dirname, 'packages/$1/src'),
},
// Important for monorepo
conditions: ['types', 'development'], // or 'production'
},
// Optimize deps for monorepo
optimizeDeps: {
include: ['@acme/ui-components', '@acme/utils'],
exclude: [], // Add problematic packages here if needed
}
});
Solution 3: ESBuild Configuration
// esbuild.config.js
const { build } = require('esbuild');
const path = require('path');
build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/bundle.js',
sourcemap: true,
resolveExtensions: ['.js', '.jsx', '.ts', '.tsx'],
alias: {
'@acme/ui-components': path.resolve(__dirname, 'packages/ui-components/src'),
'@acme/utils': path.resolve(__dirname, 'packages/utils/src'),
'@acme/api': path.resolve(__dirname, 'packages/api/src'),
'@acme/(.*)': path.resolve(__dirname, 'packages/$1/src'),
},
// Important for monorepo
platform: 'node', // or 'browser'
format: 'esm', // or 'cjs', 'iife'
// Prevent bundling monorepo packages as external
external: ['react', 'react-dom'], // but not @acme/* packages
});
Solution 4: Rollup Configuration
// rollup.config.js
import { resolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import path from 'path';
export default {
input: 'src/index.ts',
output: {
dir: 'dist',
format: 'esm',
sourcemap: true,
},
plugins: [
resolve({
// Custom resolve for monorepo paths
extensions: ['.js', '.jsx', '.ts', '.tsx'],
// Add custom resolution logic if needed
}),
commonjs(),
typescript({
tsconfig: './tsconfig.json',
// Ensure it uses the same path mapping
})
],
// Add plugin for path mapping
plugins: [
// ... existing plugins
{
name: 'resolve-monorepo-paths',
resolveId(source, importer) {
// Match @acme/* patterns
const match = source.match(/^@acme\/(.+)$/);
if (match) {
const packageName = match[1];
// Look for the package in packages/
const possiblePaths = [
path.resolve(__dirname, 'packages', packageName, 'src'),
path.resolve(__dirname, 'packages', packageName),
];
for (const possiblePath of possiblePaths) {
try {
// Check if path exists
require.resolve(`${possiblePath}/package.json`);
// Return the path to the package's main entry
return path.resolve(possiblePath);
} catch (e) {
// Continue to next path
}
}
// If not found, let other resolvers handle it
return null;
}
return null;
}
}
]
};
🧩 Using Path Mapping Libraries
Instead of manual configuration, use dedicated libraries:
Option 1: tsconfig-paths (for Node.js runtime)
# Install
npm install -D tsconfig-paths
# In your test setup or node script
require('tsconfig-paths/register');
// Usage in jest.config.js
module.exports = {
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
// ...
};
// jest.setup.js
require('tsconfig-paths/register');
Option 2: vite-tsconfig-paths (for Vite)
# Install
npm install -D vite-tsconfig-paths
# vite.config.js
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [
tsconfigPaths({
// Optional: specify tsconfig location
// projects: ['./tsconfig.base.json']
})
]
});
Option 3: tsconfig-paths-webpack-plugin (for Webpack)
# Install
npm install -D tsconfig-paths-webpack-plugin
# webpack.config.js
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = {
// ... other config
plugins: [
new TsconfigPathsPlugin({
configFile: path.resolve(__dirname, 'tsconfig.base.json')
})
]
};
Option 4: esbuild-plugin-ts-paths (for ESBuild)
# Install
npm install -D esbuild-plugin-ts-paths
# esbuild.config.js
import { tsPaths } from 'esbuild-plugin-ts-paths';
build({
plugins: [
tsPaths({
// tsconfig: './tsconfig.base.json',
// absoluteBasePath: '',
// mainFields: ['main', 'module']
})
]
// ... rest of config
});
📋 Monorepo-Specific tsconfig Best Practices
Base tsconfig (tsconfig.base.json)
{
"compilerOptions": {
/* Language and Environment */
"target": "ES2020",
"module": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ES2020"],
/* Strictness */
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
/* Path Mapping */
"baseUrl": ".",
"paths": {
"@acme/*": ["packages/*/src"],
"@acme/ui-components": ["packages/ui-components/src"],
"@acme/utils": ["packages/utils/src"],
"@acme/api": ["packages/api/src"],
"@acme/*": ["packages/*/src"], // Catch-all
/* Alternative: scoped approach */
// "@acme/ui-components/*": ["packages/ui-components/src/*"],
// "@acme/utils/*": ["packages/utils/src/*"],
},
/* Output */
"outDir": "./dist",
"rootDir": "./src",
"removeComments": false,
"noEmit": true, // For type-checking only; build tools handle emission
/* Monorepo/Workspace */
"composite": true, // Enables project references
"declareModule": true, // Allows declaring modules that aren't externally visible
"skipLibCheck": true, // Skip type checking of declaration files
/* Incremental */
"incremental": true,
"tsBuildInfoFile": "./tmp/tsbuild.info"
},
"exclude": [
"node_modules",
"dist",
"tmp"
]
}
Package-Specific tsconfig (packages/ui-components/tsconfig.json)
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "../../dist/packages/ui-components",
"rootDir": "./src",
/* Package-specific options */
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"emitDeclarationOnly": true, // If using separate build for JS
/* Module */
"module": "ESNext",
"target": "ES2020",
/* JSX if needed */
"jsx": "react-jsx",
},
"include": [
"src/**/*"
],
"exclude": [
"src/**/*.test.ts",
"src/**/*.spec.ts",
"test",
"tests"
]
}
App-Specific tsconfig (apps/web/tsconfig.json)
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "../../dist/apps/web",
"rootDir": "./src",
/* App-specific options */
"sourceMap": true,
"inlineSources": true,
/* Module */
"module": "ESNext",
"target": "ES2020",
"jsx": "preserve", // For Next.js
/* Types */
"types": ["node", "jest"], // For testing environment
},
"include": [
"src/**/*"
],
"exclude": [
"src/**/*.test.ts",
"src/**/*.spec.ts",
"next-env.d.ts",
"test",
"tests"
]
}
🔧 Package.json Configuration for Monorepos
Root package.json
{
"name": "@acme/monorepo",
"version": "1.0.0",
"private": true,
"workspaces": [
"packages/*",
"apps/*"
],
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^20.0.0",
// ... other dev deps
},
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"test": "turbo run test",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck"
}
}
Individual Package package.json
// packages/ui-components/package.json
{
"name": "@acme/ui-components",
"version": "1.0.0",
"description": "UI component library",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./components/*": {
"import": "./dist/components/*.esm.js",
"require": "./dist/components/*.js",
"types": "./dist/components/*.d.ts"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "tsc -p tsconfig.json --watch",
"test": "jest",
"lint": "eslint src --ext .ts,.tsx",
"typecheck": "tsc --noEmit"
},
"dependencies": {
// Runtime dependencies
"clsx": "^2.0.0",
"react": "^18.2.0"
},
"peerDependencies": {
// Dependencies that should be provided by consumer
"react": "^18.0.0"
},
"devDependencies": {
"@types/react": "^18.0.0",
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
}
}
🧪 Testing Configuration
Jest Configuration for Monorepo
// jest.config.js
const { pathsToModuleNameMapper } = require('ts-jest');
const { compilerOptions } = require('./tsconfig.base.json');
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
roots: ['<rootDir>/packages', '<rootDir>/apps'],
testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'],
// Map tsconfig paths to module names
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '<rootDir>/'
}),
// Setup files
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
// Mock assets
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
// Clear mocks between tests
clearMocks: true,
// Collect coverage from workspace
collectCoverageFrom: [
'packages/**/src/**/*.{ts,tsx}',
'apps/**/src/**/*.{ts,tsx}',
'!packages/**/src/**/*.d.ts',
'!apps/**/src/**/*.d.ts'
],
};
jest.setup.js
require('tsconfig-paths/register');
// Optional: setup for React Testing Library
// import '@testing-library/jest-dom';
🔄 Build Tool Integration Examples
TurboRepo (Recommended for Monorepos)
// turbo.json
{
"$schema": "https://turborepo.org/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", "build/**"]
},
"dev": {
"cache": false
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**.*"],
"outputs": ["coverage/**"]
},
"lint": {
"outputs": []
},
"typecheck": {
"dependsOn": ["^build"],
"inputs": ["tsconfig.json", "tsconfig.base.json", "src/**/*.ts", "src/**/*.tsx"]
}
}
}
Nx Configuration
// nx.json
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"npmScope": "acme",
"affected": {
"defaultBase": "main"
},
"implicitDependencies": {
"package.json": "*",
"tsconfig.base.json": "*"
},
"tasksRunnerOptions": {
"default": {
"runner": "@nrwl/workspace/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test", "lint", "typecheck"]
}
}
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"]
},
"test": {
"inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"]
},
"lint": {
"inputs": ["default", "{workspaceRoot}/.eslintrc.json", "{workspaceRoot}/.eslintignore"]
}
},
"projects": {
"ui-components": {
"root": "packages/ui-components",
"sourceRoot": "packages/ui-components/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nrwl/workspace:run-commands",
"options": {
"command": "tsc -p tsconfig.json"
}
},
"test": {
"executor": "@nrwl/jest:jest",
"options": {
"jestConfig": "packages/ui-components/jest.config.js"
}
}
}
}
// ... other projects
}
}
🐛 Debugging Path Mapping Issues
1. Check What TypeScript Sees
# List what TS resolves for a module
npx tsc --traceResolution --noEmit
# Look for lines like:
# === Module name resolution was successful. ===
# Package name: '@acme/ui-components' (looked up via package.json 'exports')
2. Verify Build Tool Resolution
# For Webpack
npx webpack --info-verbosity verbose
# Look for:
# [./src/index.ts] ./src/index.ts 123 bytes {0} [built]
# [./packages/ui-components/src/components/Button.tsx] ./packages/ui-components/src/components/Button.tsx 456 bytes {0} [built]
# For Vite
# Check dev server logs for module resolution
3. Node.js Resolution Check
// Test what Node.js would resolve
const { resolve } = require('module');
const path = require('path');
try {
const resolvedPath = resolve.sync('@acme/ui-components', {
paths: [path.resolve(__dirname, 'packages')]
});
console.log('Resolved to:', resolvedPath);
} catch (e) {
console.log('Resolution failed:', e.message);
}
4. Check Package Exports
// Check what the exports field resolves to
const { readFileSync } = require('fs');
const path = require('path');
const pkgPath = path.resolve(__dirname, 'packages/ui-components/package.json');
if (fs.existsSync(pkgPath)) {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
console.log('Exports field:', pkg.exports);
}
🚫 Common Pitfalls and How to Avoid Them
❌ Pitfall 1: Mismatched Path Patterns
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@acme/*": ["packages/*/src"] // Note: no trailing /*
}
}
}
// Usage that breaks:
import { Button } from '@acme/ui-components/Button'; // Looks for packages/ui-components/Button/src
✅ Fix: Make patterns consistent
// Option 1: Add trailing /* to match
{
"compilerOptions": {
"paths": {
"@acme/*": ["packages/*/src/*"]
}
}
}
// Then usage works:
import { Button } from '@acme/ui-components/Button';
// Option 2: Don't use wildcard in usage
{
"compilerOptions": {
"paths": {
"@acme/ui-components": ["packages/ui-components/src"],
"@acme/ui-components/*": ["packages/ui-components/src/*"]
}
}
}
import { Button } from '@acme/ui-components/Button'; // ✅ Works
❌ Pitfall 2: Forgetting to Transpile Dependencies
Problem: Building a package that imports another monorepo package without building the dependency first.
✅ Fix: Use build dependencies
// packages/api/package.json
{
"name": "@acme/api",
"version": "1.0.0",
"dependencies": {
"@acme/utils": "workspace:*" // Ensures utils is built first
}
}
With TurboRepo:
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"] // Wait for all dependencies to build
}
}
}
❌ Pitfall 3: Circular Dependencies with Path Mapping
Problem:
- Package A imports @acme/package-b
- Package B imports @acme/package-a
- Path mapping makes this seem okay, but creates runtime issues
✅ Fix:
- Detect circular dependencies with
madgeor similar - Refactor to break cycles
- Use dependency injection or shared interfaces
- Consider if packages should actually be separate
❌ Pitfall 4: Using Paths in Published Package Code
Problem:
// In packages/ui-components/src/index.ts
import { helperFunction } from '@acme/utils'; // ✅ Works in monorepo
// After publishing to npm:
// Consumer tries to import @acme/utils but gets the published version
// If @acme/utils isn't published or is different version → ERROR
✅ Fix:
- Only use path mapping for internal development
- For published packages, either: a) Depend on published versions (not recommended for tight coupling) b) Use relative paths that get resolved during publish (complex) c) Consume as local dependencies during development, publish as externals
Better approach: Use package.json "workspace:" dependencies:
// packages/ui-components/package.json
{
"name": "@acme/ui-components",
"version": "1.0.0",
"dependencies": {
"@acme/utils": "workspace:^1.0.0" // Uses local workspace version
}
}
When published, this becomes a regular version dependency.
❌ Pitfall 5: Ignoring Module Side Effects
Problem:
// Some packages have side-effectful imports
import '@acme/utils/styles/global.css'; // Registers CSS
// If path mapping doesn't preserve this during build → missing styles
✅ Fix:
- Ensure build tools treat monorepo packages as internal modules, not external
- Don't put
@acme/*inexternalarrays for bundlers - Make sure side effects are preserved during the build process
🧪 Testing Your Solution
1. Type Check Only
# Verify no TS errors
tsc --noEmit --project packages/ui-components/tsconfig.json
tsc --noEmit --project apps/web/tsconfig.json
2. Build and Check Output
# Build a package
cd packages/ui-components
npm run build
# Check that dist/ contains proper files
ls -la dist/
# Should see: index.js, index.d.ts, etc.
# Try to consume it from another package
cd ../utils
npm link ../ui-components # Link local package
npm run build # Should build without errors
3. Test in Consuming App
# Link packages for testing
cd apps/web
npm link ../ui-components
npm link ../utils
# Start dev server
npm run dev
# Should load without "Cannot find module" errors
# Check network tab - requests should go to localhost dev servers, not npm
4. Test Build Artifacts
# Build everything
npm run build
# Try to run the built app
cd dist/apps/web
node index.js # or whatever the entry point is
# Should work without module resolution errors
📋 Migration Checklist
When Setting Up New Monorepo
- [ ] Define clear path mapping strategy in tsconfig.base.json
- [ ] Match path mapping in all build tools (Webpack/Vite/Rollup/ESBuild)
- [ ] Configure workspace dependencies in package.json
- [ ] Set up proper exports fields in packages
- [ ] Configure Jest/Testing Library to respect path mappings
- [ ] Add path mapping plugins to build tools
- [ ] Set up build pipeline with proper dependencies (TurboRepo/Nx/lerna)
When Converting Existing Codebase
- [ ] Audit all imports for relative paths that should be aliased
- [ ] Replace relative imports with path-mapped imports
- [ ] Update tsconfig with appropriate path mappings
- [ ] Configure build tool aliases to match tsconfig
- [ ] Test that all tests still pass
- [ ] Verify build outputs are correct
- [ ] Check that published packages work correctly
Ongoing Maintenance
- [ ] Lint for relative path usage in monorepo internal imports
- [ ] Monitor build times - path mapping should not significantly impact performance
- [ ] Check for circular dependencies regularly
- [ ] Update path mappings when package structure changes
- [ ] Verify that new team members understand the path mapping convention
🛠️ Recommended Toolchain
Core
- TypeScript: ^5.0.0
- TurboRepo: For build pipeline and caching
- tsconfig-paths: For Node.js/runtime path resolution
- vite-tsconfig-paths or tsconfig-paths-webpack-plugin: For build tool integration
Testing
- Jest: With ts-jest and tsconfig-paths/register
- Testing Library: React/Vue/Angular variants
- ts-jest: For TypeScript testing
Code Quality
- ESLint: With plugin-import/no-unresolved to catch path issues
- Prettier: For consistent formatting
- TypeScript ESLint Parser: For TS-aware linting
Monitoring
- madge: To detect circular dependencies
- dependency-cruiser: To visualize and validate dependencies
- tsc --incremental: For fast type checking during development
💡 Advanced Techniques
1. Dynamic Path Mapping Based on Environment
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@acme/*": [
"packages/*/src", // Development
"dist/packages/*" // Production/build
]
}
}
}
Then use different tsconfig files for different contexts:
tsconfig.dev.jsonfor development (points to src/)tsconfig.build.jsonfor production (points to dist/)
2. Conditional Path Mapping
// tsconfig.override.json - extend based on conditions
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"paths": {
"@acme/*": {
// This isn't valid JSON, but conceptually:
// "if (process.env.NODE_ENV === 'development') {
// return ['packages/*/src'];
// } else {
// return ['dist/packages/*'];
// }"
}
}
}
}
In practice, use different configs or build-time generation:
# Generate appropriate tsconfig based on environment
if [ "$NODE_ENV" = "development" ]; then
cp tsconfig.dev.json tsconfig.json
else
cp tsconfig.prod.json tsconfig.json
fi
3. Using Yarn PNP (Plug'n'Play) with Path Mapping
# .yarnrc.yml
nodeLinker: pnp
pnpMode: strict
Then ensure your build tools support PNP:
- Webpack: Use
pnp-webpack-plugin - Vite: Has experimental PNP support
- Jest: Use
pnpifyor jest pnp resolver
4. Monorepo-Aware ESLint Plugin
// .eslintrc.js
module.exports = {
settings: {
'import/resolver': {
typescript: {
// This helps ESLint understand TypeScript path mapping
project: ['./tsconfig.base.json']
}
}
},
rules: {
'import/no-unresolved': ['error', {
'caseSensitive': false,
'ignore': ['^@acme/'] // Or handle via resolver above
}]
}
};
📚 Resources
Official Documentation
Tools and Libraries
- tsconfig-paths
- vite-tsconfig-paths
- tsconfig-paths-webpack-plugin
- esbuild-plugin-ts-paths
- ts-jest
- @testing-library/react
Community Articles
- "Monorepo TypeScript Path Mapping: The Complete Guide" - Various blogs
- "Solving Monorepo Module Resolution Issues" - Microsoft TypeScript team posts
- "Building Scalable Monorepos with TurboRepo" - Vercel blog
- "TypeScript Path Monorepo Best Practices" - Index.docker.io articles
TypeScript path mapping in monorepos requires alignment between tsconfig, build tools, and runtime execution. By using consistent path mapping strategies, proper build tool configuration, and workspace dependencies, you can eliminate "Cannot find module" errors while maintaining a clean, scalable monorepo architecture.