This is pretty much a minimal example to reproduce the problem. I have an Api
base class that I want to be able to instantiate the ApiObject
base class it's for. I also want the ApiObject
to hold a reference to the Api
object that created it.
class ApiObject<T extends Record<string, unknown>> {
constructor(private obj: T, private api: Api<T, ApiObject<T>>) {}
delete() {}
}
class Api<T extends Record<string, unknown>, O extends ApiObject<T>> {
constructor(private creator: new (obj: T, api: Api<T, O>) => ApiObject<T>) {}
get(obj: T) {
return new this.creator(obj, this) as O;
}
}
This is a test case that shows the problems:
type Test = { x: number };
class TestObject extends ApiObject<Test> {
change() {}
}
class TestApi extends Api<Test, TestObject> {
constructor() {
super(TestObject); // *1
}
}
const api = new TestApi();
const obj = api.get({ x: 1 });
obj.change(); // *2
As it is there, I get an error at *1
: "Property 'change' is missing in type 'ApiObject' but required in type 'TestObject'.ts(2345)".
If I remove the as O
from return new this.creator(obj, this) as O;
in Api.get
, I instead get an error at *2
: "Property 'change' does not exist on type 'ApiObject'.ts(2339)"
I use the creator
variable to allow the Api
class to create a new ApiObject
from data it gets. I'd like a reference to the Api
object so I could make calls against it within ApiObject
You can't merely say that ApiObject<T>
has an api
of type Api<T, ApiObject<T>>
, because there's nothing to say that any particular subclass of ApiObject<T>
's api
will produce the same subclass of ApiObject<T>
. Right now Api
knows about ApiObject
's subtyping, but ApiObject
doesn't know anything about Api
's subtyping. They both need to know about each other for it to work.
If subclasses of both Api
and ApiObject
need to know specifically about each other's type details to work, then you'll need to use recursive constraints (a.k.a. F-bounded quantification). You already have an ApiObject
constraint on Api
's O
parameter, but you don't have anything in ApiObject
's constraints that point to Api
or to itself. In general you might have to add both an A
(for Api
) and an O
to both classes, but it looks like you can get away with having just O
:
class ApiObject<T extends Record<string, unknown>, O extends ApiObject<T, O>> {
constructor(private obj: T, private api: Api<T, O>) { }
delete() { }
}
class Api<T extends Record<string, unknown>, O extends ApiObject<T, O>> {
constructor(private creator: new (obj: T, api: Api<T, O>) => O) { }
get(obj: T) {
return new this.creator(obj, this);
}
}
See how Api<T, O>
's creator
produces an O
, and ApiObject<T, O>
takes an Api<T, O>
, so the type information you care about is reserved. Now your subclasses need explicit references to themselves also:
type Test = { x: number };
class TestObject extends ApiObject<Test, TestObject> {
change() { }
}
class TestApi extends Api<Test, TestObject> {
constructor() {
super(TestObject); // okay
}
}
const api = new TestApi();
const obj = api.get({ x: 1 });
obj.change(); // okay
And this works. Note that sometimes you can avoid such explicit recursive generic constraints by using the polymorphic this
type, which is effectively an implicit generic type parameter that's constrained by the class. That is, the type of this
inside ApiObject<T>
would be used in place of O
. But polymorphic this
is only allowed inside instances of classes, and you'd need the constructor (which is static) for it to work here. Without something like microsoft/TypeScript#5863, it's not currently possible in TypeScript. So you need the explicit recursive generic. Annoying, but it works.