Search code examples
typescriptinheritancecircular-dependencytyping

Typescript: create various child classes from static method of parent class


I am fairly new to typescript and have ran into a problem concerning circular dependencies and child class instantiation from parent classes. I have a parent class that will ideally contain a static method which fetches various data and instantiates a certain child class depending on the retrieved data. I will have many child classes, thus I'd like to keep the children in separate files (thus the circular dependency problem). Here is some code for an example:

parent.ts

abstract class Parent {
  _data1: string;
  _data2: number;

  constructor (
    data1: string,
    data2: number,
  ){
    this._data1 = data1;
    this._data2 = data2;
  };

  static async createChildren(dataQueryURL: string): Promise<Array<Parent>> {
    const dataQuery: Array<Array<any>> = await getQueryData(dataQueryURL);
    // Suppose response looks like: [["foo", 3, 1], ["bar", 2, 2, "Hello world!"], ["baz", 5, 1]]
    
    const children: Array<Parent> = new Array<Parent>();
    dataQuery.forEach(data => {
      if(data[2] === 1){
        let child = new Child1(
          data[0],
          data[1],
          data[2],
        );
        children.push(child);
      } else if (data[2] === 2){
        let child = new Child2(
          data[0],
          data[1],
          data[2],
          data[3],
        );
        children.push(child);
      };
    });
    return children;
  };

  abstract doSomething(): number;
};

child1.ts

class Child1 extends Parent {
  _data3: number;

  constructor(
    data1: string,
    data2: number,
    data3: number,
  ){
    super(data1, data2);
    this._data3 = data3;
  };

  doSomething(): number {
    return this._data2 * this._data3;
  };
};

child2.ts

class Child2 extends Parent {
  _data3: number;
  _data4: string;

  constructor(
    data1: string,
    data2: number,
    data3: number,
    data4: string,
  ){
    super(data1, data2);
    this._data3 = data3;
    this._data4 = data4
  };

  doSomething(): number {
    return this._data2 / this._data3;
  };
};

As you can clearly see, there must be circular dependencies between parent.ts <--> child1.ts and parent.ts <--> child2.ts. Typescript throws an error at compilation time due to this.

Furthermore, when I call the static method createChild, I return an array of classes that all inherit from parent but I don't know for sure which child classes will be in the array. As a result I promise an array of parents. I have a hunch this is an incorrect way of thinking about this problem. Reason being: if I want to go through my array of children and get _data3, typescript will not expect _data3 to be contained within the classes as the parent does not contain _data3.

I can solve the circular dependencies by getting rid of the static method and creating another ts file with a function for createChildren. I can solve the _data3 typing problem by promising an array of any. However both of these seem like messy solutions. I am hoping for some insight on how to deal with both the circular dependencies and promises in a more elegant fashion. Thanks in advance!


Solution

  • You can break the compile-time import dependency by making a dynamic import (and remove the top-level import from parent.ts module)

    Something like :

      static async createChildren(dataQueryURL: string): Promise<Array<Parent>> {
        const dataQuery: Array<Array<any>> = await getQueryData(dataQueryURL);
        // Suppose response looks like: [["foo", 3, 1], ["bar", 2, 2, "Hello world!"], ["baz", 5, 1]]
        
        const children = await Promise.all(dataQuery.map(async data => {
          if(data[2] === 1){
            return new (await import('./child1')).Child1(
              data[0],
              data[1],
              data[2],
            );
          } else if (data[2] === 2){
            return new (await import('./child2')).Child2(
              data[0],
              data[1],
              data[2],
              data[3],
            );
          } else {
            throw new Error(`Unsupported type: ${data[2]}`);
          };
        }));
        return children;
      }
    

    Regarding your second question, I'm not sure to fully understand it :

    • Why not bring _data3 to Parent class if this field exists (with same type) in both Child1 and Child2 ? or make an abstract getter for this field at the Parent level ?
      In that case, you would be able to retrieve it in a generic fashion.

    • In your example, it seems like you can have responses mixing both Child1 and Child2 shaped data structures : it seems legit to me to then have a generic Array<Parent> result type.
      We may consider having a more specialized type if you would tell me that responses will always contain the same type of Child.

    Regards,