Avoiding circular references in the creation of a typesafe state management system

Exploring state management in an old project, I found that my current system utilizes MobX. Transitioning to server-side rendering (SSR) seemed straightforward in newer projects but proved troublesome with TypeScript.

The approach involved a centralized store manager overseeing all stores, allowing them to access and manipulate each other's data in JavaScript. Unfortunately, TypeScript presented challenges.

To pinpoint the issue, I created a reproducible example. Feel free to test it in the TypeScript playground to fully grasp the problem:

/**
 * The StoreManager oversees all application stores
 */
class StoreManager<T extends Record<string, InitializableStore>> {
  public stores: T = {} as any;

  constructor(public instantiators: { [K in keyof T]: (manager: any) => T[K] }) {
    for (const [name, creator] of Object.entries(instantiators)) {
      this.stores[name as keyof T] = creator(this);
    }
  }

  public async init() {
    console.info("Initializing stores");
    await Promise.all(Object.values(this.stores).map((x) => x.init()));
  }
}

export type Manager = StoreManager<Stores>;

/** 
 * Represents a store with access to the manager
 */

class InitializableStore {
  constructor(protected manager: Manager) {}

  public init(): void | Promise<void> {}
}

/** 
 * Creates a store factory helper function
 */
const createStoreFactory = <S extends InitializableStore>(
  storeClass: new (manager: Manager) => S,
) => (manager: Manager) => new storeClass(manager);

/**
 * Setting up example stores
 */

class StoreA extends InitializableStore {
  public init() {}

  public meow() {
    console.log("Meow");
  }
}

class StoreB extends InitializableStore {
  public init() {
    const { storeA } = this.manager.stores;
    storeA.meow();
  }

  public woof() {
    console.log("Woof!");
  }
}

const storeA = createStoreFactory(StoreA);
const storeB = createStoreFactory(StoreB);

/**
 * Defining the stores for the manager here
 * */
const stores = { storeA, storeB };

export type StoreMapReturn<
  T extends Record<string, (manager: Manager) => InitializableStore>
> = {
  [K in keyof T]: ReturnType<T[K]>;
};

/**
 * This results in an error due to a circular reference
 */
export type Stores = StoreMapReturn<typeof stores;

The complexity arises from the need for stores to interact with the manager while avoiding circular references. Ideally, the setup should:

  • Allow easy manager access from any store
  • Avoid relying on a global object imported externally for the manager, making it encapsulated and flexible
  • Ensure type safety when accessing stores through the manager

Answer â„–1

Essentially, the compiler needs to deduce the following sequence for the Stores type :

type Stores = typeof stores > createStoreFactory > Manager > StoreManager<Stores> > Stores 
//        ^                          circular                                      ↩

This circular reference above cannot be resolved. When hovering over the initializer for const storeA, you'll receive:

'storeA' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.

The error message explains it well: by annotating one of the const variables with an explicit type, you can break the circular type resolution (example):

type StoreFactory<T extends InitializableStore> = (manager: Manager) => T

const storeA: StoreFactory<StoreA> = createStoreFactory(StoreA)
const storeB: StoreFactory<StoreB> = createStoreFactory(StoreB)

If this repetition for each store seems excessive, an alternative approach is defining Stores first using a top-down method (example):

export type Stores = {
  storeA: StoreA;
  storeB: StoreB;
}
export type StoreFactories = { [K in keyof Stores]: (manager: Manager) => Stores[K] }

const storeA = createStoreFactory(StoreA)
const storeB = createStoreFactory(StoreB)
const stores: StoreFactories = { storeA, storeB }

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The Enigmatic Essence of TypeScript

I recently conducted a test using the TypeScript code below. When I ran console.log(this.userList);, the output remained the same both times. Is there something incorrect in my code? import { Component } from '@angular/core'; @Component({ sel ...

Why are these additional curly brackets added within an else statement?

Upon reviewing some typescript code, I noticed the presence of extra curly braces within an else block. Are these additional braces serving a specific purpose or are they simply used to provide further grouping for two nearly identical code snippets? Consi ...

The Standalone Component does not appear for debugging in webpack:source when utilizing an incompatible version of Node

I have developed two components: https://i.sstatic.net/fSNqa.png However, after running ng serve, I am only able to see one component in the source of the Chrome browser: https://i.sstatic.net/VzdDS.png How can I troubleshoot this standalone component? ...

Discovering the method to access a local function within a static function in Javascript ES6 (ES2015) or Typescript

Is there a way to access the non-static "foo2" method from inside the static "bar" method? So far, I'm only able to access the "foo1" and "foo3" methods. Can anyone provide guidance on how to achieve this? let foo1 = () => { alert('foo1†...

Creating and Injecting Singleton in Angular 2

I have a custom alert directive set up in my Angular app: import { Component } from 'angular2/core'; import { CORE_DIRECTIVES } from 'angular2/common'; import { Alert } from 'ng2-bootstrap/ng2-bootstrap'; @Component({ sele ...

Encountering difficulties while attempting to transition from angular 9 to angular 10

I attempted to upgrade my Angular project by running the following commands: $ ng update @angular/core@9 @angular/cli@9 $ ng update @angular/core @angular/cli However, when I executed the last command in the console, it resulted in an error message: Your ...

Exploring the capabilities of Typescript arrays by implementing a forEach loop in conjunction with the

I possess an array: set Array ( [0] => Array ( [name0] => J [name1] => L [name2] => C ) [1] => Array ( [data0] => 3,1,3 [data1] => 5,3 ...

What are some ways to prevent unnecessary HTML re-rendering when using this.sanitizer.bypassSecurityTrustHtml(value)?

What is the best way to prevent constant HTML re-rendering when utilizing (this.sanitizer.bypassSecurityTrustHtml(value)) in Angular versions 5 and above? ...

Having trouble executing NestJS in production mode due to missing module

After implementing a generic class as shown below, an issue seems to have arisen: import { Logger } from '@nestjs/common'; import { PaginationOptionsInterface, Pagination } from './paginate'; import { Repository } from &apo ...

Error: Unable to locate metadata for the entity "BusinessApplication"

I have been utilizing TypeORM smoothly for some time, but out of the blue, I encountered this error during an API call: EntityMetadataNotFound: No metadata for "BusinessApplication" was found. at new EntityMetadataNotFoundError (C:\Users\Rob ...

What is the method for reaching a service in a different feature module?

Currently, I am utilizing Angular 2/4 and have organized my code into feature modules. For instance, I have a Building Module and a Client Module. https://i.stack.imgur.com/LvmkU.png The same structure applies to my Client Feature Module as well. Now, i ...

Ways to limit the combination of general types in Typescript?

Struggling to develop a React form component with generic types. The initialValues parameter determines the generic type for the form. Unable to figure out how to specify the type for each field in Typescript. Check out my CodeSandbox where I've at ...

Navigating to the main directory in Angular 2

I am currently diving into the world of Angular 2 and attempting to create my very first application. I am following a tutorial from Barbarian Meets Coding to guide me through the process. Following the steps outlined in the tutorial, I have set up my appl ...

What is the best method to publish my npm package so that it can be easily accessed through JSDelivr by users?

I've been working on creating an NPM package in TypeScript for educational purposes. I have set up my parcel configuration to export both an ESM build and a CJS build. After publishing it on npm, I have successfully installed and used it in both ESM-m ...

The element is not defined in the Document Object Model

There are two global properties defined as: htmlContentElement htmlContentContainer These are set in the ngAfterViewInit() method: ngAfterViewInit() { this.htmlContentElement = document.getElementById("messageContent"); this.htmlContentCont ...

Why does the page not work when I enter a certain URL with an ID parameter, and instead displays the error message "Uncaught ReferenceError: System is not defined"?

This is my "app.routing.ts": import {provideRouter, RouterConfig} from "@angular/router"; import {DashboardComponent} from "./dashboard.component"; import {HeroesComponent} from "./heroes.component"; import {HeroDetailsComponent} from "./hero-details.com ...

Creating a structured state declaration in NGXS for optimal organization

Currently, I'm delving into the world of NGXS alongside the Emitters plugin within Angular, and I find myself struggling to grasp how to organize my state files effectively. So far, I've managed to define a basic .state file in the following man ...

The error message "Invalid export value for Next.js with TypeScript: 'getStaticProps'" indicates a problem with the entry export value

I'm currently diving into the world of Next.js and Typescript, and I've run into an error that has left me stumped as there doesn't seem to be much information available on it: "getStaticProps" is not a valid Next.js entry export value.ts( ...

Cannot instantiate Marker Clusterer as a constructor

I am facing an issue with implementing Marker Clusterer in my app. I have successfully installed '@google/markerclusterer' in my project and imported it as shown below. However, a puzzling error keeps popping up: core.js:4002 ERROR TypeError: _go ...

Prevent selection of items in ng-select. Modifying the default item selection behavior in ng-select

In my code, I am utilizing an angular-multiselect component to upload a list of items and populate the [data] variable of the angular-multiselect. This component displays the list of data with checkboxes, allowing me to select all, search, and perform vari ...