This error means TypeScript found a circular dependency between two or more modules where the type definitions are involved, preventing it from resolving the type for an exported variable.

Common Causes and Fixes for TS4023

  1. Direct Circular Type Dependency:

    • Diagnosis: Module A imports a type from module B, and module B imports a type from module A. This is the most straightforward and common cause.
      # Check imports in fileA.ts and fileB.ts
      cat fileA.ts
      cat fileB.ts
      
    • Fix: Refactor the code to break the circular dependency. Often, this involves creating a new, third module that both original modules can import from, or moving shared types to a common location.
      • Example Refactor:
        • Create sharedTypes.ts:
          // sharedTypes.ts
          export interface SharedData {
            value: string;
          }
          
        • Modify fileA.ts:
          // fileA.ts
          import { SharedData } from './sharedTypes';
          
          export const dataFromA: SharedData = { value: 'from A' };
          
        • Modify fileB.ts:
          // fileB.ts
          import { SharedData } from './sharedTypes';
          
          export const dataFromB: SharedData = { value: 'from B' };
          
      • Why it works: By extracting the shared type into its own module, neither fileA.ts nor fileB.ts directly depends on each other for type definitions, eliminating the cycle.
  2. Indirect Circular Type Dependency (via multiple files):

    • Diagnosis: Module A imports a type from B, B imports a type from C, and C imports a type from A. This is a more complex loop.
      # Inspect imports across the involved files
      cat moduleA.ts
      cat moduleB.ts
      cat moduleC.ts
      
    • Fix: Similar to direct dependency, identify the shared types or concepts that are causing the loop and extract them into a separate, dedicated module. The goal is to have a central place for these types that doesn’t create a dependency loop.
      • Example Refactor: If moduleA needs TypeFromC, moduleB needs TypeFromA, and moduleC needs TypeFromB, and these types are related, create a common.ts file.
        // common.ts
        export interface CommonInterface {
          id: number;
        }
        
        • Update moduleA.ts, moduleB.ts, moduleC.ts to import CommonInterface from common.ts instead of each other.
      • Why it works: The loop is broken by introducing an intermediary module that serves as the source for the types, so A doesn’t need to know about C directly (or vice versa) for type resolution.
  3. Namespace Circular Dependencies:

    • Diagnosis: If you’re using namespace declarations, a circular dependency can arise if one namespace imports types from another, and vice versa.
      # Examine namespace declarations
      cat nsA.ts
      cat nsB.ts
      
      • nsA.ts: namespace NS_A { import TypeFromB = NS_B.SomeType; ... }
      • nsB.ts: namespace NS_B { import TypeFromA = NS_A.AnotherType; ... }
    • Fix: Refactor namespaces to break the circularity. This might involve merging namespaces, moving common types out of namespaces, or flattening the structure. If possible, avoid complex namespace interdependencies.
      • Example Refactor: Merge related namespaces or move shared types to top-level exports.
        // mergedNamespace.ts
        export namespace SharedNamespace {
          export interface CommonType { name: string; }
        }
        
        • Then use SharedNamespace.CommonType in other files.
      • Why it works: Unifying the namespaces or extracting common types prevents one namespace from needing to know about the internal structure of another for type resolution.
  4. export * from '...' and Re-exports:

    • Diagnosis: A common pattern is index.ts files that re-export everything from other modules. If index.ts has a circular dependency with one of the modules it re-exports, the error can occur.
      # Check index files and their dependencies
      cat index.ts
      cat moduleX.ts
      
      • index.ts: export * from './moduleX';
      • moduleX.ts: import { SomeType } from './index'; (This is the problematic cycle).
    • Fix: Ensure that re-exporting modules (index.ts) do not import types from the modules they are re-exporting. Re-exports should be one-way.
      • Example Fix: If moduleX.ts needs a type that is also exported by index.ts, that type definition should be moved to a separate file that both index.ts and moduleX.ts import from.
        // shared.ts
        export interface SharedType { /* ... */ }
        
        // index.ts
        export * from './moduleX';
        export * from './shared'; // Re-export shared types
        
        // moduleX.ts
        import { SharedType } from './shared'; // Import from shared, not index
        export const myVar: SharedType = { /* ... */ };
        
      • Why it works: This ensures that moduleX.ts doesn’t create a cycle by trying to import something that is defined in the index.ts file, which in turn tries to re-export moduleX.ts.
  5. Default tsconfig.json Settings (moduleResolution, module):

    • Diagnosis: While less common as a direct cause, incorrect moduleResolution or module settings in tsconfig.json can sometimes exacerbate or mask circular dependency issues, leading to confusing errors. For example, module: "commonjs" with moduleResolution: "node" is standard, but other combinations might behave unexpectedly.
      cat tsconfig.json
      
    • Fix: Ensure your tsconfig.json has appropriate settings for your project structure and target environment. For modern projects, module: "esnext" or module: "es2020" and moduleResolution: "node" (or "bundler") are often good choices.
      • Example tsconfig.json snippet:
        {
          "compilerOptions": {
            "target": "ES2016",
            "module": "esnext",
            "moduleResolution": "node",
            "strict": true,
            "esModuleInterop": true,
            "skipLibCheck": true,
            "forceConsistentCasingInFileNames": true
          },
          // ... other settings
        }
        
      • Why it works: Correct module resolution strategies help TypeScript accurately map import paths and understand the dependency graph, making it more likely to correctly identify and report circular dependencies.
  6. Third-Party Library Issues (Rare):

    • Diagnosis: Occasionally, a poorly structured third-party library might introduce a circular dependency in its type definitions that affects your project. This is uncommon, especially with well-maintained libraries.
      # Check imports related to specific libraries
      grep "from 'your-library-name'" -r .
      
    • Fix:
      • Update the library to its latest version, as the issue might be fixed.
      • Report the issue to the library maintainers.
      • As a workaround, you might need to exclude the problematic files from type checking (tsconfig.json’s exclude or files options) or use declaration file overrides.
    • Why it works: Updating the library often resolves the underlying structural issue. Workarounds mitigate the impact on your build process.

After fixing these issues, you might encounter a TS2451: Cannot redeclare block-scoped variable '...' error if a module that was previously part of a circular dependency is now being imported in a way that causes a name collision.

Want structured learning?

Take the full Typescript course →