2

I'm quite confused about how to use the this type.

I want to ensure that arguments passed to my function base are always a super type (a subset) of the instantiated class. I use Partial<this> to do this. But when I try to invoke the function with a known shape, or interface, the compiler complains to me:

class SuperClass {

    base<Base extends Partial<this>>(args?: Base): boolean {
        return true;
    }
}

interface Bar {
    bar: string
}

class Foo extends SuperClass implements Bar {

    constructor(public bar: string) {
        super();

        this.base({ bar: "why" });
        // Argument of type '{ bar: "why"; }' is not assignable to parameter of type 'Partial<this>'.ts(2345)

        this.base<Bar>();
        // Type 'Bar' does not satisfy the constraint 'Partial<this>'.ts(2344)
    }
}

With the same code, the following is fine:

const f: Partial<Foo> = { bar: "" }

Can anyone explain why this fails specifically when used in the Foo class?

Sebastian Nemeth
  • 5,505
  • 4
  • 26
  • 36
  • The issue is essentially the same as [in this question](https://stackoverflow.com/q/46980763/2887218). The `this` type is an implicit generic type parameter which stands for any possible subclass type, and you can always narrow properties, as shown [in this playground link](https://tsplay.dev/NdEkQm); the value `{bar: "why"}` is not assignable to the type `{bar: "oopsieDoodle"}`. Does that fully address your question? If so I could write up an answer explaining; if not, what am I missing? – jcalz Mar 16 '23 at 03:40
  • Yes the link does answer my question, thanks very much! – Sebastian Nemeth Mar 16 '23 at 13:32

1 Answers1

1

The polymorphic this type is an implicit generic type parameter corresponding to the current context. Inside a method implementation in a class body, this is constrained to the class instance type, but is otherwise unspecified. The compiler will treat it just like a generic type parameter inside a generic function, and not let you do something with it unless it's sure it would be applicable to all possible narrowings of that type. This improves type safety in the face of possible subclasses. On the other hand, in a method call, the this type is specified with the type of the instance on which you call the method. So the compiler will allow you to do anything that works for that particular class and is not concerned about possible subclasses.

This is very similar to how generic functions behave:

function f<T extends string>(x: T): T {
    return "x"; // error
}
const x: "x" = f("x"); // okay

There return "x" is an error because T can be any subtype of string; it might not be "x". But f("x") does produce the type "x" when you call it. This makes sense because nothing stops someone from writing

const y: "y" = f("y"); // okay, but bad function impl

and it would be unfortunate if a value of type "y" turned out to be "x" at runtime.

For the same reason then, you can't assume this inside the body of Foo is exactly Foo; it could be some subtype of Foo. You might be thinking that's still okay because surely all subtypes will have a bar property of type string, right? And Partial<this> should clear up any concerns about other properties, right?

Well, no... properties themselves can be narrowed. So bar could be narrower than string, as shown here:

class Baz extends Foo {
    readonly bar = "oopsieDoodle";
}

And that means

new Baz("huh"); 

would end up passing { bar: "why" } in a place that expects { bar: "oopsieDoodle" }, and that's an error. See this question and its answer for the same general issue for explicitly generic functions.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Great answer! I got it from your first comment, but this lays it out beautiful, thank you. In your code example, did you mean to write `readonly bar = "oopsieDoodle" as const;` in order to narrow string to a literal? – Sebastian Nemeth Mar 17 '23 at 16:16
  • Using `readonly` in a class property will cause such a narrowing without the need for `as const`. – jcalz Mar 17 '23 at 16:21