Home > Net >  Typescript: why is a class generic not assignable to same generic inferred from `this`?
Typescript: why is a class generic not assignable to same generic inferred from `this`?

Time:05-12

Given the following code, why does Typescript error in the getInferred method? Is there a case where ValueOf<this> and T could be different?

interface Wrapper<T> {
    value: T;
}

type ValueOf<T> = T extends Wrapper<infer U> ? U : never;

class Foo<T> implements Wrapper<T> {
    value: T;

    constructor(value: T) {
        this.value = value;
    }

    getInferred = (): ValueOf<this> => {
        // Type 'T' is not assignable to type 'GetGeneric<this>'.
        return this.value;
    }

    getSimple = (): T => {
        // Works Fine
        return this.value;
    }
}

For my use-case, I'm adding methods dynamically to a class and ValueOf<this> provides better return types for the dynamic methods.

const mixin = {
    getFooInferred<Self extends Foo<any>>(this: Self) {
        return this.getInferred();
    },
    getFooSimple<Self extends Foo<any>>(this: Self) {
        return this.getSimple();
    }
}

function makeFooWithMixin<T>(value: T) {
    const foo = new Foo(value);

    Object.defineProperties(foo, {
        getFooInferred: {
            value: mixin.getFooInferred,
        },
        getFooSimple: {
            value: mixin.getFooSimple,
        }
    });

    return foo as Foo<T> & typeof mixin;
}

const foo = makeFooWithMixin("hello")

// When using the returntype of `getInferred`, we correctly get `string` as the type here
const resultInferred = foo.getFooInferred()

// When using `getSimple`, we instead get `any` because `getFooSimple` types the `Self` generic as `Foo<any>`
const resultSimple = foo.getFooSimple();

Typescript playground link for all above code

CodePudding user response:

The polymorphic this type is implemented as an implicit generic type parameter that all classes and interfaces have (see microsoft/TypeScript#4910). And your ValueOf<T> type, defined as

type ValueOf<T> = T extends Wrapper<infer U> ? U : never;

is a conditional type. So ValueOf<this> is a conditional type that depends on a generic type parameter.

And unfortunately, the TypeScript compiler is unable to do much reasoning about what values will be assignable to such a type. It defers evaluation of the type and can only know what it really is once this is specified, such as in the call new Foo("x").getInferred(), where this will be Foo<string>. Inside the body of getInferred(), this is unspecified (it can be any subtype of Foo<T>), and so ValueOf<this> is essentially opaque to the compiler. It isn't that this.value can be of a type other than ValueOf<this>, but rather that the compiler cannot see it. It will reject any value that is not already of type ValueOf<this>.

If you use a type assertion like this.value as ValueOf<this>, then the compiler will allow you to return that, but only because you are claiming that this.value is of type ValueOf<this>, and not because the compiler can tell one way or the other:

getInferred = (): ValueOf<this> => {
    return this.value as ValueOf<this>; // okay
}

In general, if you need to provide a value of a generic conditional type, you'll have to do something unsafe like a type assertion. But in this particular instance, you have an alternative. All you're doing with ValueOf<T> is looking up the value-keyed property in T. And this can be done without conditional types. You can use an indexed access type instead:

type ValueOf<T extends Wrapper<any>> = T['value']

And even though the compiler still isn't great at understanding arbitrary manipulations of generic types, it does know that if you have a value of type T and a key of type K that the property value you read at that key will be of type T[K], even if T or K are generic. So it should be able to verify that this.value is of the type this["value"]:

getInferred = (): ValueOf<this> => {
    return this.value; // okay
}

And indeed it can.

Playground link to code

  • Related