Claude-skills typescript-circular-dependency
git clone https://github.com/ckorhonen/claude-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/ckorhonen/claude-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/continuous-learning/examples/typescript-circular-dependency" ~/.claude/skills/ckorhonen-claude-skills-typescript-circular-dependency && rm -rf "$T"
skills/continuous-learning/examples/typescript-circular-dependency/SKILL.mdTypeScript Circular Dependency Detection and Resolution
Problem
Circular dependencies occur when module A imports from module B, which imports (directly or indirectly) from module A. TypeScript compiles successfully, but at runtime, one of the imports evaluates to
undefined because the module hasn't
finished initializing yet.
Context / Trigger Conditions
Common error messages:
ReferenceError: Cannot access 'UserService' before initialization
TypeError: Cannot read properties of undefined (reading 'create')
TypeError: (0 , _service.doSomething) is not a function
Symptoms that suggest circular imports:
- Import is
even though the export existsundefined - Error only appears at runtime, not during TypeScript compilation
- Moving an import statement changes which import is undefined
- Tests fail but the app works (or vice versa)
- Adding
at the top of a file changes behaviorconsole.log
Solution
Step 1: Detect the Cycle
Use a tool to visualize dependencies:
# Install madge npm install -g madge # Find circular dependencies madge --circular --extensions ts,tsx src/ # Generate visual graph madge --circular --image graph.svg src/
Or use the TypeScript compiler:
# Check for cycles (requires tsconfig setting) npx tsc --listFiles | head -50
Step 2: Identify the Pattern
Common circular dependency patterns:
Pattern A: Service-to-Service
services/userService.ts → services/orderService.ts → services/userService.ts
Pattern B: Type imports
types/user.ts → types/order.ts → types/user.ts
Pattern C: Index barrel files
components/index.ts → components/Button.tsx → components/index.ts
Step 3: Resolution Strategies
Strategy 1: Extract Shared Dependencies
Before:
// userService.ts import { OrderService } from './orderService'; export class UserService { ... } // orderService.ts import { UserService } from './userService'; export class OrderService { ... }
After:
// types/interfaces.ts (new file - no imports from services) export interface IUserService { ... } export interface IOrderService { ... } // userService.ts import { IOrderService } from '../types/interfaces'; export class UserService implements IUserService { ... }
Strategy 2: Dependency Injection
// orderService.ts export class OrderService { constructor(private userService: IUserService) {} // Instead of importing UserService directly } // main.ts const userService = new UserService(); const orderService = new OrderService(userService);
Strategy 3: Dynamic Imports
// Only import when needed, not at module level async function processOrder() { const { UserService } = await import('./userService'); // ... }
Strategy 4: Use Type-Only Imports
If you only need types (not values), use type-only imports:
// This doesn't create a runtime dependency import type { User } from './userService';
Strategy 5: Restructure Barrel Files
Before (problematic):
// components/index.ts export * from './Button'; export * from './Modal'; // Modal imports Button from './index'
After:
// components/Modal.tsx import { Button } from './Button'; // Direct import, not from index
Step 4: Prevent Future Cycles
Add to your CI/build process:
// package.json { "scripts": { "check:circular": "madge --circular --extensions ts,tsx src/" } }
Or configure ESLint:
// .eslintrc.js module.exports = { plugins: ['import'], rules: { 'import/no-cycle': ['error', { maxDepth: 10 }] } }
Verification
- Run
- should report no cyclesmadge --circular src/ - Run your test suite - previously undefined imports should work
- Delete
and reinstall - app should still worknode_modules - Build for production - no runtime errors
Example
Problem:
OrderService is undefined when imported in UserService
Detection:
$ madge --circular src/ Circular dependencies found! src/services/userService.ts → src/services/orderService.ts → src/services/userService.ts
Fix: Extract shared interface
// NEW: src/types/services.ts export interface IOrderService { createOrder(userId: string): Promise<Order>; } // MODIFIED: src/services/userService.ts import type { IOrderService } from '../types/services'; export class UserService { constructor(private orderService: IOrderService) {} } // MODIFIED: src/services/orderService.ts // No longer imports UserService export class OrderService implements IOrderService { async createOrder(userId: string): Promise<Order> { ... } }
Notes
- TypeScript
is your friend—it's erased at runtime and can't cause cyclesimport type - Barrel files (
) are a common source of accidental cyclesindex.ts - The order of exports in a file can matter when there's a cycle
- Jest/Vitest may handle module resolution differently than your bundler
- Some bundlers (Webpack, Vite) have better cycle handling than others
can sometimes mask circular dependency issues thatrequire()
exposesimport