I am wondering why I am receiving
Type 'typeof TestBody' does not satisfy the constraint 'typeof AbstractBodyWithTabs'.
Construct signature return types 'TestBody' and 'AbstractBodyWithTabs<T, U>' are incompatible.
The types of 'contents' are incompatible between these types.
Type 'TestContent' is not assignable to type 'T'.
'TestContent' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'AbstractSelector'.
class TestPage extends PageWithTabs<typeof TestBody> {
The "T" constraint is an extended class of the correct type.
export class AbstractSelector {
protected readonly myRoot : Selector
constructor(inSelector : Selector) {
this.myRoot = inSelector;
}
}
export abstract class AbstractBodyWithTabs<
T extends AbstractSelector,
U extends Tabs
> {
abstract get contents () : T
abstract get tabs () : U
get wrapper () : Selector {
return Selector(".app-component");
}
}
export abstract class Tabs extends AbstractSelector {
get active () {
return this.wrapper.find(":scope > button[contains(@class, 'focus')]");
}
get all () {
return this.wrapper.find(":scope > button");
}
abstract get named_tab () : Record<string, Selector>;
get wrapper () {
return this.myRoot.find("div.btn-group");
}
}
export abstract class PageWithTabs<V extends typeof AbstractBodyWithTabs> {
abstract get body () : InstanceType<V>
get footer () {
return new SaveAndCancelFooter();
}
get header () {
return new Header();
}
}
class TestContent extends AbstractSelector {
}
class TestTabs extends Tabs {
get named_tab () {
return { get country () { return Selector("div"); } };
}
}
class TestBody extends AbstractBodyWithTabs<TestContent, TestTabs> {
get contents () {
return new TestContent(this.wrapper);
}
get tabs () {
return new TestTabs(this.wrapper);
}
}
class TestPage extends PageWithTabs<typeof TestBody> {
get body () {
return new TestBody();
}
}
When I make these changes:
export abstract class PageWithTabs<V extends typeof AbstractBodyWithTabs> {
abstract get body () : InstanceType<V>
get footer () {
return new SaveAndCancelFooter();
}
get header () {
return new Header();
}
}
to
export abstract class PageWithTabs<T extends AbstractSelector, U extends Tabs, V extends AbstractBodyWithTabs<T, U>> {
abstract get body () : V
get footer () {
return new SaveAndCancelFooter();
}
get header () {
return new Header();
}
}
class TestPage extends PageWithTabs<typeof TestBody> {
get body () {
return new TestBody();
}
}
to
class TestPage extends PageWithTabs<TestContent, TestTabs, TestBody> {
get body () {
return new TestBody();
}
}
It works, but I don't like how I have to pass multiple types that have already been declared.
typescript playground
CodePudding user response:
The error
typeof AbstractBodyWithTabs
is roughly equivalent* to
type TypeofAbstractBodyWithTabs =
new <T extends AbstractSelector, U extends Tabs>() => AbstractBodyWithTabs<T, U>;
This means if I have a typeof AbstractBodyWithTabs
, I can choose whatever T
and U
I want (as long as they extend AbstractSelector
and Tabs
):
class NotTestContent extends AbstractSelector {
foo = "foo";
}
class NotTestTabs extends Tabs {
bar = "bar";
get named_tab() {
return {};
}
}
function f(AbstractBodyWithTabsCtor: typeof AbstractBodyWithTabs): void {
const bodyWithTabs = new AbstractBodyWithTabsCtor<NotTestContent, NotTestTabs>();
bodyWithTabs.contents.foo;
bodyWithTabs.tabs.bar;
}
Notice how T
is NotTestContent
and U
is NotTestTabs
.
What if we try doing this with typeof TestBody
?
function g(TestBodyCtor: typeof TestBody): void {
const bodyWithTabs = new TestBodyCtor();
bodyWithTabs.contents.foo;
// ~~~
// Property 'foo' does not exist on type 'TestContent'.
bodyWithTabs.tabs.bar;
// ~~~
// Property 'bar' does not exist on type 'TestTabs'.
}
bodyWithTabs.contents
is TestContent
, unlike in f
where we can make it NotTestContent
using the type parameters of AbstractBodyWithTabsCtor
.
This explains 'TestContent' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'AbstractSelector'.
from the error message: T
could be another subtype of AbstractSelector
such as NotTestContent
, not necessarily TestContent
.
The solution
abstract class PageWithTabs<V extends AbstractBodyWithTabs<AbstractSelector, Tabs>> {
abstract get body (): V
// ...
}
If the type parameters of AbstractBodyWithTabs
didn't have any constraints, you would use unknown
instead of AbstractSelector
and Tabs
. The only reason why AbstractSelector
and Tabs
are used here is due to the constraints on T
and U
; if you tried to use unknown
you would get Type 'unknown' does not satisfy the constraint 'AbstractSelector'.
.
Now this works fine because TestBody
is assignable to AbstractBodyWithTabs<AbstractSelector, Tabs>
.
class TestPage extends PageWithTabs<TestBody> { /* ... */ }
Note that that V extends AbstractBodyWithTabs<AbstractSelector, Tabs>
does not mean that T
in AbstractBodyWithTabs
is exactly AbstractSelector
and U
is exactly Tabs
. It means that the T
and U
types just need to be assignable to AbstractSelector
and Tabs
respectively.
TestContent
extends AbstractSelector
and TestTabs
extends Tabs
, so TestBody
which extends AbstractBodyWithTabs<TestContent, TestTabs>
is assignable to AbstractBodyWithTabs<AbstractSelector, Tabs>
.
*typeof AbstractBodyWithTabs
is more like abstract new <T extends AbstractSelector, U extends Tabs>() => AbstractBodyWithTabs<T, U>
(notice the abstract
). This means f
doesn't actually compile because you can't construct an abstract class. However, I think the principle of what I described above still holds true for abstract constructors.
Regardless, here's an example of typeof AbstractBodyWithTabs
working where typeof TestBody
doesn't:
function f(AbstractBodyWithTabsCtor: typeof AbstractBodyWithTabs): void {
class MyBodyWithTabs extends AbstractBodyWithTabsCtor<NotTestContent, NotTestTabs> {
get contents(): NotTestContent {
return new NotTestContent(new Selector(""));
}
get tabs(): NotTestTabs {
return new NotTestTabs(new Selector(""));
}
}
}
function g(TestBodyCtor: typeof TestBody): void {
class MyBodyWithTabs extends TestBodyCtor {
get contents(): NotTestContent {
return new NotTestContent(new Selector(""));
}
get tabs(): NotTestTabs {
// ~~~~
// Property 'tabs' in type 'MyBodyWithTabs' is not assignable to the same property in base type 'TestBody'.
// Type 'NotTestTabs' is not assignable to type 'TestTabs'.
// Types of property 'named_tab' are incompatible.
// Property 'country' is missing in type '{}' but required in type '{ readonly country: Selector; }'.
return new NotTestTabs(new Selector(""));
}
}
}