Home > OS >  why does my type not satisfy the constraint
why does my type not satisfy the constraint

Time:10-09

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>.

Playground link


*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(""));
        }
    }
}
  • Related