How to Detect and Fix Circular Dependencies in Typescript
Tags: typescript, javascript, frontend
Published: 2024-03-15 10:39:09

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:

```typescript
npm i -g typescript
```

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

```bash
mkdir circular-references
cd circular-references
tsc --init
```

## **Create The Types**

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

```typescript
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**

```typescript
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.

```typescript
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.

```typescript
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:

```typescript
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`.

```bash
$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](https://www.npmjs.com/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\]](https://www.npmjs.com/package/madge)

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

```bash
npx madge -c --extensions ts ./
```


![1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qd4u7zt3v9dxib7zsznk.png)
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`.

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

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

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

### **Update the** `button.ts` File

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

```typescript

import { Theme } from "./theme";

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

### **Create 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`.

```typescript
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 widget again and voila! 🎉


![2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jl25by8cy4ryjjoj0m1u.png)

## 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.
    

This 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 🚀

* [source code](https://github.com/danywalls/fix-circular-references)