Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does this mapped/conditional type behave differently when inferring the type of "this" vs receiving it explicitly?

Consider the following code, which uses TypeScript language features introduced in v2.8 (conditional types):

type P<TObject, TPropertySuperType> = {
    [K in keyof TObject]: TObject[K] extends TPropertySuperType ? K : never;
}[keyof TObject];

function g<
    T,
    K extends keyof Pick<T, P<T, string>>
    >(obj: T, prop: K): void { }

class C {
    public alpha: string;
    public beta: number;

    public f(): void {
        g(this, "alpha"); // <-- does not compile!
        g(this, "beta");

        g<C, "alpha">(this, "alpha");
        g<C, "beta">(this, "beta");

        g(new C(), "alpha");
        g(new C(), "beta");

        this.g2("alpha");
        this.g2("beta");

        this.g2<"alpha">("alpha");
        this.g2<"beta">("beta");
    }

    public g2<
        K extends keyof Pick<C, P<C, string>>
        >(prop: K) { }
}

The idea behind the type P is that it selects the names of the properties of TObject that satisfy the constraint that the type of the property extends TPropertySuperType. The functions g and g2 then use the type P in a type parameter constraint, such that:

  • You can only call g when the prop parameter is the name of a extends string-typed property of obj
  • You can only call g2 when the prop parameter is the name of a extends string-typed property of C.

Here, because C.alpha is of type string and C.beta is of type number, I would expect all five invocations of g/g2 with prop === "alpha" to compile, and all five invocations with prop === "beta" not to compile.

However, the invocation g(this, "alpha") does not compile, as you can see if you paste this code into the TypeScript playground. The error is:

Argument of type '"alpha"' is not assignable to parameter of type 'this[keyof this] extends string ? keyof this : never'.

Why does this particular invocation fail? I'm guessing it has something to do with how TypeScript infers the type of this, but the details are fuzzy to me.

like image 419
senshin Avatar asked Aug 21 '18 00:08

senshin


1 Answers

I agree with arthem the most likely culprit is polymorphic this. While obvious the type of this will be polymorphic this. While you can say for sure C['alpha'] is of type string, for this['alpha'] you can't say that, all you can say is that this['alpha'] extends string which is a more complicated relation for the compiler to follow. Not sure if this analogy helps, but polymorphic this acts like a hidden type parameter to the class, and using it is subject to similar limitations. For example, inside g the type of obj['prop'] is not known to be string again because of limitations of what can be said for generic type parameters:

function g<
    T,
    K extends keyof Pick<T, P<T, string>>
    >(obj: T, prop: K): void {  obj[prop].charAt(0) /*error*/}

While the above is speculation (and I'll admit a bit fuzzy), the solution that solves the error above will solve the issue with this namely to put our constraint that only string keys can be passed in in a different way.

function g3<
    K extends string | number | symbol,
    T extends Record<K, string>
    >(obj: T, prop: K): void {   obj[prop].charAt(0) /* ok*/ }

class C {
    public alpha!: string;
    public beta!: number;

    public f(): void {
        g3(this, "alpha"); // also ok as expected
        g3(this, "beta"); //not ok
    }
}
like image 135
Titian Cernicova-Dragomir Avatar answered Jan 03 '23 12:01

Titian Cernicova-Dragomir