kennarddh
kennarddh

Reputation: 2665

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.

Upvotes: 1

Views: 62

Answers (2)

jcalz
jcalz

Reputation: 329893

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

Upvotes: 1

Maxime C.
Maxime C.

Reputation: 21

If you don't need your class to be generic, you could just remove the generic part, and use the ReturnType utility type like so:

class Controller  {
  public main(body: ReturnType<Controller["getBody"]>) {
    console.log({ body })
  }

  public getBody() {
    return { a: 'b' }
  }
}

This will give you the autocomplete you want.

Hope it helps!

Upvotes: 0

Related Questions