Skip to content

Error when passing this to generic interface where Type Parameter has been extended #35498

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
rdhelms opened this issue Dec 4, 2019 · 7 comments
Labels
Duplicate An existing issue was already created

Comments

@rdhelms
Copy link

rdhelms commented Dec 4, 2019

TypeScript Version: 3.7.2

Search Terms:
Passing this to Generic Interface with extended Type Parameter

Code

namespace ErrorDemo {
    interface IRelation<T extends ITest> {
        relatedThing: T
    }

    interface ITest<T extends object = object> {
        relation: IRelation<this>   // Error: Type 'ITest<T>' is not assignable to type 'ITest<object>'.
        validate<U extends T>(thingToValidate: U): object extends T ? object : U
    }

    function doesNotUseTypeParam(thing: ITest) {
        const validated = thing.validate({ randomProp: 100 })  // object - this is what we want
    }

    function usesTypeParam(thing: ITest<{ example: string }>) {
        const validated = thing.validate({ example: 'hello' })  // { example: string }
    }
}

namespace NoErrorButNotDesired {
    interface IRelation<T extends ITest> {
        relatedThing: T
    }

    interface ITest<T extends object = object> {
        relation: IRelation<this>   // No error
        validate<U extends T>(thingToValidate: U): U    // Note that `object extends T` is no longer here
    }

    function doesNotUseTypeParam(thing: ITest) {
        const validated = thing.validate({ randomProp: 100 })  // { randomProp: number }, but wanted object
    }

    function usesTypeParam(thing: ITest<{ example: string }>) {
        const validated = thing.validate({ example: 'hello' })  // { example: string }
    }
}

Expected behavior:
In the above example, we want the return type of validate to be different based on whether the user passed a type parameter to the interface. An intuitive way to test this seems to be to check whether the parameter's default type extends the parameter itself (which theoretically should only be true if the parameter IS the default).

Actual behavior:
Mysteriously, as soon as object extends T is used for the type of validate, an error is thrown on the this parameter being passed to the generic IRelation interface. The types in the functions ARE actually shown to be what we want them to be, despite the error.

It's possibly worth pointing out that the above example is clearly a simplified example to demonstrate the error. Our actual use case involves trying to have the return type of an interface's function have more specific control over whether an object's keys are required or not. The above example, while likely not useful in the real world, is a very close representation of what we're doing.

Playground Link:
http://www.typescriptlang.org/play/?ssl=1&ssc=1&pln=38&pc=1#code/HYQwtgpgzgDiDGEAEBRATmg9mgIhMmSA3gLABQSlSAlsAC4RoBmCyAkgEoQA2Id1mYAB4AKkggAPBsAAmUJGxHQ6APmLkqmpGh58IMkQAtaAcwBcSERqoBfctcq0GzVgqVQ6o8VIiz5mACMAKwh4OiQAXiRAkLC1UgotSh1efkELTl004TpjKDVKAHpC1AxsCxEATxhkAHJFZVEVWpp5YExwkCgoahNQAO5kOkI6arqGjyEY0NVagDoHLQA3EG5qGT0hAFVvaTlLFQAKXNMRTAA1VfW9Cy2ASgtpsN3ffbEAfmjgmaRbxbsyIsmABXYBhATAJAyTDQAByHS2UAgVRqAAUQGhwMdjMBzG5lHd1IkkvBBB4kCs1hsGDJIkgTri5pTrgxDkRtCBZJgwKisDALABGAAMQqQNkJSGKX1idH+9mJSBBYOySGBSKgKIg6MxYGxpgy7k87Mk4BggwsHjQpjFKkJCSSlFJwHJzOp+jpDJMTKubrZ3lN5qQtUMPG4mBa4qKJWNEgDEAtdCtuLFcrIAPIoEgsFc8PQWDQACFgXR4XQ8D0dLT7VQnIwWIgFFxUhCvJI9vIJqoiQ7tFl9EZ9ZZU4tay4G53Wz4-NKflEnl3q0kUnwIRkmyvBEITvkqFL4eIymhFppXZsdm3XvIREdPWdLlSbkh7rdNHuOkNDHwkAADecvaciN+rRIO0SBhrijBICGOjDgqSrgoIUIwlApaIsiYzalinoGgS3YOk6Lo+notJRJ63oPqy7KYlyPJ8oKIpihKUpUZy0K0Zg-IgcCYABJBNgADRIAExZIAA7pyNIzmEsGaPBKpqtAmqYbq2H4pMMZxgmSYmDadrHlQBHhKekmkTiXrGRAfommAZrxkGIbcGGEZMdG-o2YGlrWgCmgAgCQA

Related Issues:
I've seen various comments that I thought might be related, but they all seem to deal with the contravariance of keyof - but I'm not aware of how keyof could possibly be related to the above example.

@jack-williams
Copy link
Collaborator

Technically this is a duplicate of #31251.

TLDR: The conditional type in the return of validate marks T as invariant. You can fix this using the following (requires --strictFunctionTypes).

interface ITest<T extends object = object> {
    relation: IRelation<this>;
    validate<U extends T>(thingToValidate: U): ((x: T) => void) extends ((x: object) => void) ? object : U;
}

@rdhelms
Copy link
Author

rdhelms commented Dec 4, 2019

Wow @jack-williams that is spectacularly effective (while also spectacularly confusing 🙂)

That's definitely enough for me to roll with for now - thanks a ton!

@rdhelms
Copy link
Author

rdhelms commented Dec 4, 2019

Ok - follow up question...

I see that your workaround does indeed help us accomplish what we want, but is that ONLY working because of the bug that you linked? When that issue is fixed, will this workaround still work?

Another way of asking this: in this conditional, is Test meant to be true or false?

type Test = ((param: true) => void) extends ((param: boolean) => void) ? true : false

@rdhelms
Copy link
Author

rdhelms commented Jan 6, 2020

@jack-williams Do you happen to have any other info in response to my previous follow up question?

Basically I'm trying to figure out what the eventual behavior will theoretically be after #31251 is fixed

@jack-williams
Copy link
Collaborator

Sorry @rdhelms!

Another way of asking this: in this conditional, is Test meant to be true or false?

Test should always be false under strict function types, and there are no issues that will change that.

The workaround I posted works independently of the bug in #31251, as that only targets conditional types with parameters in extends types.

The workaround is not 100% perfect because the type as written is fundamentally unsound. In IRelation<this> the parameter this is not guaranteed to satisfy the constraint ITest<object>. If you ignore the return type in validate you end up trying to relate:

 validate<U extends T>(thingToValidate: U): unknown // ignore return

to

 validate<U extends object>(thingToValidate: U): unknown // ignore return

which is not sound because the constraint in the target is less specific than the source. A minimal example is:

const a: <U extends 3>(x: U) => void = (x) => {
    // x can only be 3
}

const b: <U extends number>(x: U) => void = a; // error, otherwise x could be any number

The proposed workaround works because check types in conditional types are related bivariantly, so when you measure the variance for the modified version T comes out as bivariant, where previously it was invariant. For example, the following should error, but it does not:

declare const foo: ITest<{ x: 'string' }>;
const bar: ITest<{ x: string }> = foo;

That being said, there are currently no PR's planned to address the variance for conditional check-types, so there is nothing planned that will break the workaround. I think alot of code could face regressions if this ever changed.

@rdhelms
Copy link
Author

rdhelms commented Jan 7, 2020

Thanks @jack-williams that's a very useful example and discussion. I think I'm only beginning to start to wrap my head around the different contexts and subtleties for how TS checks for invariance, but this helps clarify a few points. I appreciate the notes.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Jan 15, 2020
@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants