The Challenge
You’re using pnpm monorepo workspaces and TypeScript. You want to use path aliases (like @shared/*), but during build you hit conflicts especially when multiple packages or apps define the same alias, or when Next.js/Vite can’t resolve them as expected.
The Anatomy of a Conflict
- Duplicate Alias Names: Multiple packages define the same alias (e.g. @/ or @shared/*), so it’s ambiguous during build.
- Alias doesn’t match Package Name: The alias doesn’t match the package name so the bundler or TS can’t resolve it.
- Bundler/TSConfig mismatch: TS and your bundler (Next.js, Vite, etc.) have different alias configs and you get runtime or build errors.
- No Unique Package Names: Packages don’t have unique names in package.json so aliasing and resolution is fragile.
Step-by-Step: The Conflict-Free Path
Avoid path alias conflicts in monorepos by following this clean, structured approach to naming, aliasing, and configuration across tools.
1. Give Every Package a Unique Name
json
// packages/shared/package.json{
"name": "@yourorg/shared",
"version": "1.0.0"
}
Unique names prevent collision and aid in path mapping.
2. Use Distinct Aliases Per Package
- Don’t use the same alias (like @/) in multiple projects.
- Instead, scope each alias to its package:
json
// packages/ui/tsconfig.json{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@ui/*": ["./src/*"]
}
}
}
// apps/web/tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"], // app-local
"@ui/*": ["../../packages/ui/src/*"] // shared package
}
}
}
This ensures each alias points to exactly one place and avoids overlap.
3. Mirror Alias Config in Your Bundler
js
// next.config.jsconst withTM = require('next-transpile-modules')(['@yourorg/shared']);
module.exports = withTM({ /* ... */ });
For Vite:
js
// vite.config.tsimport path from 'path';
export default {
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@ui': path.resolve(__dirname, '../../packages/ui/src'),
},
},
};
Always keep alias settings in sync between tsconfig and your bundler.
Also Read: How to Convert HTML Website to ReactJS?
4. Reference the Correct Paths in tsconfig
- In the consuming app’s tsconfig.json, point aliases to the relative path of the shared package’s source:
json
{ "compilerOptions": {
"paths": {
"@yourorg/shared/*": ["../../packages/shared/src/*"]
}
}
}
This enables both type-checking and runtime resolution.
5. Avoid Overlapping Wildcard Aliases
- Never use a catch-all alias like @/* in more than one package it will cause conflicts.
- Always scope with the package name or a unique prefix.
Common Pitfalls & Quick Fixes
Pitfall
|
Example |
Fix
|
Duplicate alias @/ in many |
All packages use @/*: ./src/* |
Use @ui/*, @shared/*, etc., per package |
Alias not matching package |
Alias: @shared/*, package: ui |
Match alias to package: @ui/* for ui package |
Only tsconfig, not bundler |
Alias in tsconfig, not in Vite/Next.js |
Mirror alias config in both tsconfig and bundler |
No unique package name |
"name": "shared" |
"name": "@yourorg/shared" in package.json |
Pro Tips
- If you use Next.js, always transpile shared packages with next-transpile-modules.
- If you hit persistent conflicts, consider using relative imports as a last resort—they’re less pretty, but always work.
- For large monorepos, tools like Nx or Turborepo help manage project references and aliases at scale.
- Keep your tsconfig and bundler configs DRY: Use a base config and extend it in each package/app.