Repo guidance: Avoid circular dependencies
Circular dependencies (A imports B, B imports A) between files are legal in TypeScript and ES modules but introduce non-determinism at bundle time, which can result in undefined exports or order-sensitive bugs. Some bundlers have known edge cases where cycles degrade tree-shaking or cause execution order problems.
Basic example of a cycle:
ts
// a.ts
import { bFn } from './b'
export const aValue = 1
export function aFn() {
return bFn() + aValue
}
// b.ts
// This import reads before aFn executes fully. The constant `aValue` may be
// initialized, but more complex patterns (class inheritance, mutated singletons)
// become fragile.
import { aValue } from './a'
export function bFn() {
return aValue
}Why cycles hurt
- Initialization ordering: Partially evaluated modules can expose incomplete objects.
- Performance: Extra parsing work to establish strongly connected components.
- Tool fragility: Edge-case bugs surface more often inside cycles.
Common sources
- Barrel files (
index.ts) that re-export siblings and are imported back by those siblings. - Overly large utility modules re-exported widely.
- Cross-layer leakage (e.g. UI layer importing data layer which imports UI types).
Detection
Use dependency graph tooling (e.g. madge) in CI to fail on new cycles.
Example madge usage:
bash
madge src --circularRefactoring strategies
- Extract shared types/constants into a new leaf module with no imports.
- Invert dependencies via callbacks or interfaces.
- Split large modules into smaller, directional layers.
- Replace barrel-based intra-package imports with direct relative imports.