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:
@ahejlsberg thoughts?
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.
Oops, I got my variance backwards in the example code... I'll update it
Edit: all right, I updated it.
See #36261 for an improved method of measuring variance.
Most helpful comment
See #36261 for an improved method of measuring variance.