Search code examples
typescript

How to infer a class method unknown return type and use it in a property


I'm developing a framework where some of the types are unknown since the user can return anything from an abstract method.

In this case, I want to infer what the user is returning from initialize method, and use that type in a property of the same class. This initialize method must return a class that extends from Base.

The code would be something like this:

class Base {
  method_1() {}
}

class Child extends Base {
  method_2() {}
}

abstract class AbstractClass {
  prop: C

  abstract initialize(): C extends Base
}

class UserClass1 extends AbstractClass {
  initialize() {
    return new Base()
  }

  someMethod() {
    this.prop.method_1()  // Valid, no TS error
    this.prop.method_2()  // Error, it doesn't exist in Base
  }
}

class UserClass2 extends AbstractClass {
  initialize() {
    return new Child()
  }

  someMethod() {
    this.prop.method_1()  // Valid, no TS error
    this.prop.method_2()  // Valid, no TS error
  }
}

In resume, I have to infer C type from initialize method (which extends from Base) and apply this type to prop.

Can this be done?


Solution

  • You can use the polymorphic this type to express the type of the "current" subclass of AbstractClass. And then we can say that prop's type is the return type of the initialize method of this:

    abstract class AbstractClass {
      prop!: ReturnType<this["initialize"]>
      abstract initialize(): Base
    }
    

    Notice that ReturnType<this["initialize"]> is using the ReturnType utility type to extract the return type from a function type, and an indexed access type to look up the type of the initialize property in this.

    Let's test it out:

    class UserClass1 extends AbstractClass {
      initialize() {
        return new Base()
      }
    
      someMethod() {
        this.prop.method_1()  // Valid, no TS error
        this.prop.method_2()  // Error, it doesn't exist in Base
      }
    }
    
    class UserClass2 extends AbstractClass {
      initialize() {
        return new Child()
      }
    
      someMethod() {
        this.prop.method_1()  // Valid, no TS error
        this.prop.method_2()  // Valid, no TS error
      }
    }
    

    Looks good. In each case, prop is given the type ReturnType<this["initialize"]>, which for UserClass1 is known only to be assignable to Base, whereas for UserClass2 it's known to be assignable to Child.

    Playground link to code