Skip to content
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

Function-like recursive generic interface invariant in its parameter type #35805

Closed
jcalz opened this issue Dec 20, 2019 · 4 comments · Fixed by #36261
Closed

Function-like recursive generic interface invariant in its parameter type #35805

jcalz opened this issue Dec 20, 2019 · 4 comments · Fixed by #36261
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue

Comments

@jcalz
Copy link
Contributor

jcalz commented Dec 20, 2019

I'm aware that this is likely to be a design limitation; I can't find an existing issue about this specific problem so I'm filing this one for reference.

TypeScript Version: 3.8.0-dev.20191220

Search Terms: variance, composition

Code

interface Fn<A, B> {
    (a: A): B;
    then<C>(next: Fn<B, C>): Fn<A, C>;
}

declare const fn: Fn<string, number>;
const contravariantInA: Fn<"a", number> = fn; // error!
// Type 'Fn<string, number>' is not assignable to type 'Fn<"a", number>'.
// Type 'string' is not assignable to type '"a"'.

const covariantInB: Fn<string, unknown> = fn; // okay

Expected behavior: Fn<A, B> should be contravariant in A.

Actual behavior: Fn<A, B> is invariant in A.

I came upon this while investigating a Stack Overflow question in which someone had built a composable type like Fn<A, B> and had to use a workaround to allow the then() method to accept something with a wider input.

@jack-williams suggests that the issue happens because the checker defaults to covariance for A when checking it (in typeArgumentsRelatedTo())... and combined with its contravariant use in the call signature, this results in invariance.

A full structural check might not have this problem but I imagine it might be hard to guarantee that such a check would terminate? (e.g., Fn<A, B> depends on Fn<B, C> which depends on Fn<C, D> which depends on Fn<D, E> etc?)

If there's no plausible way for the compiler to correctly infer the variance here, perhaps this is just another request for variance annotations as per #1394?

Not sure. Anyway, thanks!

Playground Link: Provided

Related Issues:
#1394: let us mark the parameters as covariant/contravariant/invariant ourselves
#32674: a different variance issue
#33872: a different variance bug

@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.9.0 milestone Jan 13, 2020
@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Jan 13, 2020
@RyanCavanaugh
Copy link
Member

@ahejlsberg thoughts?

@ahejlsberg
Copy link
Member

First off, looks like we measure Fn<A, B> as invariant in A and covariant in B:

declare const fn: Fn<string, number>;

// Invariant in A
const fn1: Fn<unknown, number> = fn;  // Error
const fn2: Fn<'a', number> = fn;  // Error

// Covariant in B
const fn3: Fn<string, unknown> = fn;  // Ok
const fn4: Fn<string, 0> = fn;  // Error

This is pretty much the expected result. As we attempt to relate the recursive references to Fn in the then method, the covariant default kicks in. The combination of that and the measured contravariance from the reference to A in the call signature gives us invariance for A. Would be nice to do better, but if we completely remove the default-to-covariance recursion guard and structurally compare until the entire variance measurement is complete, we have repro examples that never terminate. So, we'd need something smarter, like perhaps a depth based mechanism that only activates the recursion guard after a few levels.

We could consider variance annotations, but we would then also need logic to verify that type declarations don't violate them.

@jcalz
Copy link
Contributor Author

jcalz commented Jan 14, 2020

Oops, I got my variance backwards in the example code... I'll update it

Edit: all right, I updated it.

@jcalz jcalz changed the title Function-like recursive generic interface invariant in both type parameters Function-like recursive generic interface invariant in its parameter type Jan 14, 2020
@ahejlsberg ahejlsberg added Bug A bug in TypeScript Fix Available A PR has been opened for this issue and removed Needs Investigation This issue needs a team member to investigate its status. labels Jan 17, 2020
@ahejlsberg
Copy link
Member

See #36261 for an improved method of measuring variance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants