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:
theme.ts
definedTheme
and wanted to useButtonConfig
(which requiredTheme
).button.ts
definedButtonConfig
that depended onTheme
fromtheme.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',
};
Update the button.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;
};
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
.
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! π
What We did ?
Yes, we fixed the circular dependency by making a small structural change:
theme.ts
is independent and defines the baseTheme
type and objectsDarkTheme
andLightTheme
.button.ts
depends ontheme.ts
for theTheme
type.theme-config.ts
depends on bothbutton.ts
for theButtonConfig
type andtheme.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 π
Top comments (2)
What's the problem though? Because tsc didn't report an error and the app is working as expected you said? This shouldn't be an issue because modules are only imported once and imported modules are available in the global scope, so their import order doesn't matter. Unless both modules need to initialize something that requires a variable from the other module that's only set after initialization. In that case you will get a runtime error.
next js fast refresh might break due to circular references