Infer generic `this` in a base class method

101 views Asked by At

I cannot make sense of how TS is inferring this generic this: playground

class Base<T> {
    baseMethod<This extends Base<T>>(this: This): 123 extends This ? { x: 1, y: 1 } : { x: 2, z: 3 } {
        return {} as any;
    }
}

class Derived<T> extends Base<T> {
    derivedMethod() {
        const res = this.baseMethod();
        res.x; // 1 | 2
        res.z; // ERROR: property does not exist
    }
}

How is it possible that it's returning the union of both branches of the type conditional? I'd expect to return the second one (or even the first one, if This was somehow inferred as any or unknown. But both?)

Note 1: If I remove <This extends Base<T>>(this: This) and simply use 123 extends this instead (I guess they are equivalent and I don't need This), the result is the same, it returns the union.

Note 2: As @caTS mentioned, if you explicitly write derivedMethod(this: Derived<T>) then it works. But why would TS need that?

1

There are 1 answers

0
kikon On

The error seems to be the result of the fact that typescript defers to compute types derived from this inside methods because those types might be altered in further derived classes. This prevents it in the current version to perform even legitimate evaluation, as expected in @tokland's question.

If we consider the example

class B {
    f() {
        let o: {x: number} extends this ? { x: 1, y: 1 } : { x: 2, z: 3 } = {} as any;
        o.x; // 1 | 2
        o.y; // ERROR: property does not exist
    }
}

the deferment is fully justified, since B doesn't satisfy the condition {x: number} extends B, but a derived class

class C extends B{
    n = 22
}

does. (playground)

This is similar to the case in this typescript issue.

The problem is that in some cases there's no need to defer computing with this type; that is the case in the code from OP's question, or in this "extreme" example

class B {
    f() {
        let o: never extends this ? { x: 1, y: 1 } : { x: 2, z: 3 } = {} as any;
        o.x; // 1 | 2
        o.y; // ERROR: property does not exist
    }
}

(playground)

I suppose we may expect from (ask for?) future releases to improve at this point.

OP's question may be reduced (I'll show in the end the steps I took) to the following, more familiar version of B:

class B {
    f() {
        let o: 123 extends this ? { x: 1, y: 1 } : { x: 2, z: 3 } = {} as any;
        o.x; // 1 | 2
        o.z; // ERROR: property does not exist
    }
}

It may be corrected as per @caTS's suggestion by setting the type of this in the method, and (surprisingly for me) this case also requires replacing this as a type - which still remains untouched - with typeof this.

class BErr2 {
    f(this: BErr2) {
        let o: 123 extends this ? { x: 1, y: 1 } : { x: 2, z: 3 } = {} as any;
        o.x; // 1 | 2
        o.z; // STILL ERROR!
    }
}

class BOk {
    f(this: BOk) {
        let o: 123 extends typeof this ? { x: 1, y: 1 } : { x: 2, z: 3 } = {} as any;
        o.x; // 1 | 2
        o.z; // OK
    }
}

(playground)

The steps I took to simplify the code from the question: