This TypeScript error occurs when project references create a circular dependency chain, preventing the compiler from determining build order. Fix it by restructuring your project references to eliminate cycles, using composite projects, or adjusting tsconfig.json files.
The "Circular dependency between project references" error occurs in TypeScript when you have a circular chain of project references in your TypeScript configuration. Project references allow TypeScript projects to depend on other projects, but they must form a directed acyclic graph (DAG) - meaning no cycles are allowed. When TypeScript encounters circular project references, it cannot determine a valid build order. For example, if Project A references Project B, Project B references Project C, and Project C references Project A, the compiler gets stuck in an infinite loop trying to figure out which project to build first. This error typically appears when running `tsc --build` or when using composite projects with references. The compiler detects the cycle and stops with this error message to prevent infinite recursion or undefined build behavior.
First, examine your project structure to identify where the circular dependency occurs. Check all tsconfig.json files in your project:
// Example tsconfig.json with references
{
"compilerOptions": {
"composite": true
},
"references": [
{ "path": "../project-b" } // Check if this creates a cycle
]
}Look for any project that references another project which eventually references back to the original. You can create a visual diagram or list all references to trace the chain.
Restructure your project dependencies to eliminate the cycle. Consider these approaches:
1. Extract shared code: Move common code that both projects depend on into a third shared project
2. Merge projects: If two projects are tightly coupled, consider merging them into one
3. Change dependency direction: Determine which project should truly depend on the other
Example of fixing a direct cycle:
Before: Project A ↔ Project B (circular)
After: Project A → Shared → Project B (no cycle)For complex monorepos, use TypeScript's composite project feature properly:
// In root tsconfig.json
{
"files": [],
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/ui" },
{ "path": "./packages/utils" }
]
}Each sub-project should have "composite": true and should only reference projects it truly depends on, not projects that depend on it.
After restructuring, verify the fix works:
1. Run tsc --build --verbose to see the build order
2. Check that all projects compile successfully
3. Test that your application still works correctly
4. Consider adding a pre-build script to detect cycles:
#!/bin/bash
# Simple cycle detection (pseudo-code)
# Check that no project references create cycles
# You could write a script that parses all tsconfig.json filesImplement practices to prevent circular dependencies from reoccurring:
1. Document dependency rules: Clearly define which projects can depend on others
2. Use dependency graphs: Tools like madge or dependency-cruiser can detect cycles
3. CI/CD checks: Add cycle detection to your build pipeline
4. Code reviews: Review tsconfig.json changes carefully when adding new references
Example using dependency-cruiser:
npx dependency-cruiser --config .dependency-cruiser.json src/Create a .dependency-cruiser.json config file to define allowed and forbidden dependencies.
## Advanced Considerations
### TypeScript Build Mode
When using tsc --build, TypeScript performs several optimizations:
- Incremental compilation across projects
- Proper ordering of build steps
- Shared type checking between projects
Circular dependencies break these optimizations because the compiler cannot determine a valid topological order.
### Monorepo Tooling Integration
If you're using monorepo tools like:
- Nx: Has built-in dependency graph management
- Lerna: May need custom scripts for TypeScript project references
- Turborepo: Supports task dependencies that can help prevent cycles
These tools often provide better dependency management than manual tsconfig.json references.
### Performance Implications
Circular dependencies can cause:
- Slower build times due to repeated compilation
- Increased memory usage from duplicated type checking
- Difficulty with incremental builds
### Alternative Approaches
For some use cases, consider:
1. Path mapping: Use paths in tsconfig instead of project references for simple cases
2. Single composite project: Combine related code into one larger project
3. Build tool integration: Let your build tool (Webpack, Rollup) handle dependency resolution
### Debugging Tips
Use tsc --build --dry --verbose to see what TypeScript would build without actually building. This can help identify the circular chain.
Function expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript