How to Detect and Fix Circular Dependencies in Typescript

How to Detect and Fix Circular Dependencies in Typescript

Solving and Identifying Circular Dependencies in Typescript

ยท

5 min read

When coding in TypeScript or Angular and embracing types, sometimes we don't pay attention to "Circular dependencies". We create types and try to use them everywhere to match with the existing structure, and if we're not careful, it's so easy to create a circular reference and dependency loop that can be problematic in our code.

Today, I want to show how easy it is to create circular dependencies and how to fix them.

Scenario

We are building an app that shows buttons based on a theme. The buttons must have a theme and a label, and the user can set a config or use a default.

We are using TypeScript. To ensure you have TypeScript installed, if not, run the command:

npm i -g typescript

After that, create a new directory from the terminal and generate a default TypeScript config with tsc --init:

mkdir circular-references
cd circular-references
tsc --init

Create The Types

First, we create theme.ts with the properties related to colors and fontFormat.

export type Theme = {
  color: string;
  fontFormat: string;
};

Next, because we want to keep our code split, we create the file button.ts for the buttons. The button type has two properties: label as a string and theme of type Theme.

Remember to import the Theme

import { Theme } from "./theme";

export type ButtonConfig = {
    label: string;
    theme: Theme;
};

This code looks fine without any circular dependency, perfect, but now it's time to continue with our project.

Using The Theme

We want to provide two types of themes: Dark and Light, and a default configuration for the users, defining a list of buttons based on the theme. Open again theme.ts and create the themes.

export const DarkTheme: Theme = {
    color: 'black',
    fontFormat: 'italic'
};

export const LightTheme: Theme = {
    color: 'white',
    fontFormat: 'bold'
};

Next, for the defaultConfig, set to use a theme with a list of buttons based on the theme. Let's import the ButtonConfig and assign the theme.

import { ButtonConfig } from "./button";

export const defaultThemeConfig: ThemeConfig = {
    buttons: [
        {
            theme: DarkTheme,
            label: 'Accept'
        },
        {
            theme: DarkTheme,
            label: 'Cancel'
        }
    ],
    type: 'dark'
};

Everything seems to be working as expected.

  • Split the types for buttons and themes into separate files.

  • Created light and dark themes.

  • Set up a default configuration.

It looks like our code is functioning correctly. Let's go ahead and use it.

Build Dummy App

We create an example app with the function to show buttons based on the config. If there's no config, then use the defaultThemeConfig.

Create the main.ts file and import the defaultThemeConfig. Create the app with a function showButtons, inside check if config doesn't exist use the defaultThemeConfig.

The code looks like this:

import { defaultThemeConfig } from "./theme-config"; 

const app = {
    config: undefined,
    showButtons() {
        const config = this.config ?? defaultThemeConfig;
        console.log(config);
    }
};
app.showButtons();

In the terminal, compile main.ts using tsc and run with node.

$circular-references>tsc main.ts
$circular-references>node main.js
{
  buttons: [
    { theme: [Object], label: 'Accept' },
    { theme: [Object], label: 'Cancel' }
  ],
  type: 'dark'
}

Yeah! Our app works as expected, but wait a minute. Did you see theme.ts requires button.ts and button.ts uses theme.ts?

Circular Reference ๐Ÿ˜–

We created a circular reference. Why? Because the buttons require the theme and vice versa. Why did this happen?

Because we created the Theme and ThemeConfig in the same file, while also having a dependency on ButtonConfig.

The key to the circular dependency was:

  1. theme.ts defined Theme and wanted to use ButtonConfig (which required Theme).

  2. button.ts defined ButtonConfig that depended on Theme from theme.ts.

In my case, it's easy to see, but if you have an already large project, the best way to see your circular dependency is with the package madge, it reports all files with circular dependencies.

Madge is amazing tool to generating a visual graph of project dependencies, finding circular dependencies and giving other useful information [read more]

In our case, run the command npx madge -c --extensions ts ./.

npx madge -c --extensions ts ./

Ok I have the issue how to fix ?

Fixing Circular Dependency

To fix the circular reference issue between theme.ts and button.ts, we must to create a new file to break the relations to ensure that the dependencies between these files are unidirectional, extracting the common dependencies into a separate file. In our case we can move all related to theme config ThemeConfig and default configuration in a new file theme-config.ts.

Create a specific file for ThemeConfig, help us to keeps the theme-related configurations separate from the theme and button definitions.

Let's to refactor

The Theme

The theme.ts only needs to export types contain the definitions for Theme and the theme instances like DarkTheme and LightTheme.

export type Theme = {
  color: string;
  fontFormat: string;
};

export const DarkTheme: Theme = {
  color: 'black',
  fontFormat: 'italic',
};

export const LightTheme: Theme = {
  color: 'white',
  fontFormat: 'bold',
};

Thebutton.ts File

Now, modify the button.ts file to type, which relies on Theme from theme.ts.


import { Theme } from "./theme";

export type ButtonConfig = {
    label: string;
    theme: Theme;
};

Theme-config.ts

Create theme-config.ts its contain the ThemeConfig and the default configuration for the theme, utilizing ButtonConfig from button.ts and indirectly, Theme from theme.ts.

import { ButtonConfig } from "./button";
import { DarkTheme } from "./theme";

export type ThemeConfig = {
    type: 'dark' | 'light';
    buttons: Array<ButtonConfig>;
};

export const defaultThemeConfig: ThemeConfig = {
    buttons: [
        {
            theme: DarkTheme,
            label: 'Accept'
        },
        {
            theme: DarkTheme,
            label: 'Cancel'
        }
    ],
    type: 'dark'
};

Run the madge again and voila! ๐ŸŽ‰

What We did ?

Yes, we fixed the circular dependency by making a small structural change:

  • theme.ts is independent and defines the base Theme type and objects DarkTheme and LightTheme.

  • button.ts depends on theme.ts for the Theme type.

  • theme-config.ts depends on both button.ts for the ButtonConfig type and theme.ts for the theme objects, bringing them together into a configuration object.

We eliminate the circular dependency by organizing the code into a more linear dependency : theme.ts โž” button.ts โž” theme-config.ts. Each file has a clear responsibility, and the dependency direction is from the definition of the theme and button configuration.

I hope this helps you think about and fix your circular dependencies, or even avoid them altogether in the future ๐Ÿš€

ย