node.jstypescripttsccircular-reference

Resolving Typescript circular reference with () => TypeName


Run into circular reference issues with Typescript recently. https://github.com/microsoft/TypeScript/issues/27519 suggests using () => TypeName but how do I use the variable?

export default abstract class State {
    protected readonly _logger: ILogger;
    protected readonly _name: StatusEnum;
    protected readonly _colour: string;
    protected readonly _context: StateContext; // XXX: Error here
    protected constructor(@inject(LoggerTypes.ILogger) logger: ILogger, name: StatusEnum, context: () => StateContext) {
        this._logger = logger;
        this._name = name;
        this._context = context;
        this._colour = StatusColors[this._name] || StatusColors[StatusEnum.NEW]
    }
    public abstract handle (): void;
    public Name (): StatusEnum { return this._name }
}

Build error:

error TS2322: Type '() => StateContext' is not assignable to type 'StateContext'.

15         this._context = context;
                           ~~~~~~~

  Domain/States/State.ts:15:25
    15         this._context = context;
                               ~~~~~~~
    Did you mean to call this expression?

Answer provided by Fisherman seems to work. However, it hits the following error in State subclass:

export default class NewState extends State {
    private readonly _quoted: State;
    public constructor(@inject(LoggerTypes.ILogger) logger: ILogger, context: () => StateContext) {
        super(logger, StatusEnum.NEW, context);
        this._quoted = new QuotedState(logger, this._context); // XXX: Error here
    }
    public override async handle () {
        // Do operation
        this._logger.Log(LogLevels.debug, "NewState.handle()")
        await this._context.ChangeState(this._quoted);
    }
}

Errors:

Domain/States/NewState.ts:12:48 - error TS2345: Argument of type 'StateContext' is not assignable to parameter of type '() => StateContext'.
  Type 'StateContext' provides no match for the signature '(): StateContext'.

12         this._quoted = new QuotedState(logger, this._context);
                                                  ~~~~~~~~~~~~~

Domain/States/QuotedState.ts:14:52 - error TS2345: Argument of type 'StateContext' is not assignable to parameter of type '() => StateContext'.
  Type 'StateContext' provides no match for the signature '(): StateContext'.

14         this._approved = new ApprovedState(logger, this._context);
                                                      ~~~~~~~~~~~~~

Domain/States/QuotedState.ts:15:52 - error TS2345: Argument of type 'StateContext' is not assignable to parameter of type '() => StateContext'.
  Type 'StateContext' provides no match for the signature '(): StateContext'.

15         this._rejected = new RejectedState(logger, this._context);

Solution

  • You need to call the context function to get the StateContext instance before assigning it to this._context. Here's how you can do it:

    typescript

    export default abstract class State {
        protected readonly _logger: ILogger;
        protected readonly _name: StatusEnum;
        protected readonly _colour: string;
        protected readonly _context: StateContext; // No error here
        protected constructor(
            @inject(LoggerTypes.ILogger) logger: ILogger, 
            name: StatusEnum, 
            context: () => StateContext // Context is a function returning StateContext
        ) {
            this._logger = logger;
            this._name = name;
            this._context = context(); // Call the function to get the StateContext instance
            this._colour = StatusColors[this._name] || StatusColors[StatusEnum.NEW];
        }
        public abstract handle (): void;
        public Name (): StatusEnum { return this._name; }
    

    UPDATE

    In your State abstract class, _context should be defined as a function that returns StateContext rather than StateContext directly. Modify the State class to store the context function instead of calling it immediately.

    export default abstract class State {
        protected readonly _logger: ILogger;
        protected readonly _name: StatusEnum;
        protected readonly _colour: string;
        protected readonly _context: () => StateContext; // Store the function instead of StateContext itself
        protected constructor(@inject(LoggerTypes.ILogger) logger: ILogger, name: StatusEnum, context: () => StateContext) {
            this._logger = logger;
            this._name = name;
            this._context = context; // Store the function
            this._colour = StatusColors[this._name] || StatusColors[StatusEnum.NEW];
        }
        public abstract handle(): void;
        public Name(): StatusEnum { return this._name; }
    }
    

    When you need to access the StateContext instance in subclasses, you must call the _context function to get the actual instance. Update your NewState class accordingly:

    export default class NewState extends State {
        private readonly _quoted: State;
        public constructor(@inject(LoggerTypes.ILogger) logger: ILogger, context: () => StateContext) {
            super(logger, StatusEnum.NEW, context);
            this._quoted = new QuotedState(logger, context); // Pass the context function, not this._context
        }
        public override async handle() {
            // Do operation
            this._logger.Log(LogLevels.debug, "NewState.handle()");
            await this._context().ChangeState(this._quoted); // Call _context() to get the StateContext instance
        }
    }
    

    Fix Other Subclasses:

    Similarly, update any other subclasses to pass the context function and call it when accessing StateContext properties or methods:

    typescript

    export default class QuotedState extends State {
        private readonly _approved: State;
        private readonly _rejected: State;
    
        public constructor(@inject(LoggerTypes.ILogger) logger: ILogger, context: () => StateContext) {
            super(logger, StatusEnum.QUOTED, context);
            this._approved = new ApprovedState(logger, context); // Pass the context function
            this._rejected = new RejectedState(logger, context); // Pass the context function
        }
    
        public override async handle() {
            this._logger.Log(LogLevels.debug, "QuotedState.handle()");
            // Example of using the context
            await this._context().ChangeState(this._approved); // Call _context() to get the StateContext instance
        }
    }
    

    Calling the context Function:

    Ensure that whenever you need to access properties or methods of StateContext, you do so by calling the this._context() function:

    typescript

    // Correct way to access the StateContext instance
    this._context().ChangeState(this._quoted);