How do intersection types work in conjunction with polymorphic this and generics?

Viewed 87

The following code snippet doesn't compile, and I don't understand why. Sorry that it's a little verbose, this is the smallest MWE I could find.

interface Num {
    num: number;
}

class Base {
    num?: number;
    base(): this {
        return this;
    }

    addNum(): this & Num {
        this.num = 1;
        return this as this & Num;
    }
}

function unwrapNum<B extends Base>(base: B): B & Num {
    return base.addNum().base();
}

it fails with error:

Type 'B' is not assignable to type 'B & Num'.
  Type 'Base' is not assignable to type 'B & Num'.
    Type 'Base' is not assignable to type 'B'.
      'Base' is assignable to the constraint of type 'B', but 'B' could be instantiated with a different subtype of constraint 'Base'.
        Type 'B' is not assignable to type 'Num'.
          Type 'Base' is not assignable to type 'Num'.
            Types of property 'num' are incompatible.
              Type 'number | undefined' is not assignable to type 'number'.
                Type 'undefined' is not assignable to type 'number'.

My understanding is the input type is B after calling addNum it should turn into B & Num which it seems to if I break out that assignment. However, when I call base() on that, the result doesn't have type B & Num as I expect / want, but instead just B, which implies that something isn't working as expected. My hunch is that this has to do with the polymorphic this, e.g. (B & Num).base() does not have type B & Num.

However, if I remove the generics with something like:

const base = new Base();
const numed: Base & Num = base.addNum();
const numed2: Base & Num = numed.base();

that works just fine, so it feels like it's a combination of the generic Base, polymorphic this, and intersection type.

Stangely, this does compile if wrapped in another generic:

function unwrapNum<B extends Base>(base: B): B & Num {

    function helper<BN extends B & Num>(b: BN): BN {
        return b.base();
    }

    return helper(base.addNum());
}

Thanks Aleksey L. for the smaller MWQ

0 Answers
Related