Search code examples
typescriptclassgenericstypescript-typingstypescript-generics

How to infer class generic from its function's return type


How can I infer a class generic from it's own method

class Controller<Body extends Record<string,any>> {
  public main(body: Body) {
    // This should have auto complete too for the body.a
    console.log({ body })
  }

  public getBody(): Body {
    return {a:'b'} //error
  }
}

// This should not need any generic parameter
const controller = new Controller()

// This should have auto complete
controller.main({a:'b'})

The error is

Type '{ a: string; }' is not assignable to type 'Body'. '{ a: string; }' is assignable to the constraint of type 'Body', but 'Body' could be instantiated with a different subtype of constraint 'Record<string, any>'.(2322)

I removed some of the code. Originally it uses zod object for the body. Also this class should be abstract but I removed that too to make it simpler.


Solution

  • Generics in TypeScript can't be used to do what you're trying to do. Generic class type arguments are chosen by the caller of the constructor, not the implementer of the class. If Controller is generic in Body, that means calling new Controller() is when Body gets specified, not inside getBody(). Trying to use generics for this purpose is backwards.

    Really you do not want Controller to be generic. If you want to try to compute the return type of getBody() from the implementation, you could use the indexed access type Controller["getBody"] to get the type of the method, and then the ReturnType utility type, like ReturnType<Controller["getBody"]>.

    Bet even this is much more complicated and not a common approach. Generally speaking people just create named types to represent things they want to reuse:

    interface Body {
      a: string;
    }
    
    class Controller {
      public main(body: Body) {
        console.log({ body })
      }
    
      public getBody(): Body {
        return { a: 'b' }
      }
    }
    
    const controller = new Controller()
    controller.main({ a: 'b' })
    

    Or, if you really need to compute the type, move that logic out of the class so you don't create a possibly circular dependency:

    const body = { a: "b" };
    type Body = typeof body;
    
    class Controller {
      public main(body: Body) {
        console.log({ body })
      }
    
      public getBody(): Body {
        return body;
      }
    }
    
    const controller = new Controller()
    controller.main({ a: 'b' })
    

    This is the same basic type, but the Body type is computed.

    Playground link to code