javascripttypescriptdomain-driven-designcqrs

Class field is not being set


I'm building my own CQRS library and I seem to have an issue understanding why a certain field is not being correctly set. More specifically, the issue seems to be present in classes that extend my AggregateRoot abstract class. See the following Typescript code:

import Entity from "./entity";
import Event from "./event";

export enum AggregateRootConstructionTypes {
    Create,
    Load
}

type CreateConstructorParams = {
    type: AggregateRootConstructionTypes.Create,
    createParams?: any;
}

type LoadConstructorParams = {
    type: AggregateRootConstructionTypes.Load,
    history: Event[]
}

type AggregateRootConstructorParams = CreateConstructorParams | LoadConstructorParams;

abstract class AggregateRoot extends Entity {
    private _events: Event[] = [];
    private _version: number = -1;

    get domainEvents(): Event[] {
        return this._events;
    }

    get version(): number {
        return this._version;
    }

    addDomainEvent(event: Event): void {
        this._events.push(event);
    }

    clearEvents(): void {
        this._events.splice(0, this._events.length);
    }

    protected apply(event: Event): void {
        this._events.push(event);
        this._version++;
        this.when(event);
    }

    protected abstract when(event: Event): void;
    protected abstract create(input: any): void;

    protected loadFromHistory(history: Event[]): void {
        history.forEach((event) => {
            this.when(event);
            this._version++;
        });
    }

    constructor(params: AggregateRootConstructorParams) {
        super();
        if (params.type === AggregateRootConstructionTypes.Create) {
            this.create(params.createParams);
        } else {
            this.loadFromHistory(params.history);
        }
    }
}

export default AggregateRoot;

The issue is in the following test

import AggregateRoot, {AggregateRootConstructionTypes} from "./aggregate-root";
import Event from "./event";

describe('AggregateRoot', () => {
   class CreatedEvent extends Event {}

    class ExtendedAggregateRoot extends AggregateRoot {
       create() {
           const createdEvent = new CreatedEvent();
           this.apply(createdEvent);
       }

       someState: number = 0;

       private handleCreatedEvent() {
           this.someState = 1;
       }

       protected when(event: Event) {
           if (event instanceof CreatedEvent) {
               this.handleCreatedEvent();
           }
       }
   }

    describe('when applying an event', () => {
       it('should update the version, register an event, and trigger the handler in the when() function', () => {
           const EAR = new ExtendedAggregateRoot({ type: AggregateRootConstructionTypes.Create });
           expect(EAR.version).toEqual(0);
           expect(EAR.domainEvents).toHaveLength(1);
           expect(EAR.someState).toEqual(1); // ----------> this line fails
       });
    });
});

the last expect statement fails. Please help me understand why. I tried logging along the way and it oddly states that some someState seems to be set to 1. but when asserting the value, it seems like its still set to 0. This makes me believe that I potentially may have lost execution context with how I have written my code but I can't seem to identify where. Any help is appreciated.


Solution

  • This depends on your TypeScript compiler configuration.

    The TypeScript documentation for type-only field declarations states:

    When target >= ES2022 or useDefineForClassFields is true, class fields are initialized after the parent class constructor completes, overwriting any value set by the parent class.

    In this example, the sequence of events is:

    1. new ExtendedAggregateRoot({ type: AggregateRootConstructionTypes.Create }) is called
    2. The AggregateRoot constructor invokes this.create(params.createParams)
    3. create in ExtendedAggregateRoot invokes this.apply(createdEvent)
    4. apply in AggregateRoot invokes this.when(event)
    5. when in ExtendedAggregateRoot invokes this.handleCreatedEvent()
    6. handleCreatedEvent in ExtendedAggregateRoot sets this.someState = 1
    7. The constructor returns
    8. ExtendedAggregateRoot "initialises" someState to 0

    You have a number of options for fixing this problem. My personal recommendation would be to refactor to separate the concerns of the entity state data from the mechanism of applying events. Rather than making the state a field of the ExtendedAggregateRoot class, make it a separate data structure that is passed to the parent constructor, and have your when method take the current state and return a new state formed by applying the event to the current state.