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

2

7

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.

Hervey answered 21/8, 2018 at 0:49 Comment(0)
R
2

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
    }
}
Ruttish answered 21/8, 2018 at 5:0 Comment(0)
F
1

It's hard to tell without looking at the compiler sources, but one thing is clear: this inside C does not have type C, based on the observation that error goes away if you add type assertion this as C:

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

    public f(): void {
        g(this as C, "alpha"); // ok

The type this[keyof this] spelled out in the error message in place for TObject[K] hints at this being "polymorphic this" type - the type that represents all possible subtypes of C.

I don't know what causes this to be treated as polymorphic here (maybe it's always assumed to be polymorphic), but it has an unfortunate consequence that set of its keys is not statically known.

Although it can be proven that even with polymorphic this, "alpha" will always be among its keys and this["alpha"] type will be compatible with string, as it's declared in C, the compiler does not follow the necessary logic, and just plays it safe - "if it's not statically known it's incompatible".

Funkhouser answered 21/8, 2018 at 1:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.