TypeScript Version: 3.5.1
Search Terms:
generic interface, invariant, contravariant, type arg, parameter list, function property
Code
//strictFunctionTypes enabled
declare let _a : {
//This is a function property, not a method
_ : (arg : ["a", "b", bigint]) => void
};
declare let _b : {
//This is a function property, not a method
_ : (arg : ["a", "b", bigint|null]) => void
};
/**
* Assignment OK!
*/
_a = _b;
/**
* Assignment not allowed, as expected
*
* Type '["a", "b", bigint | null]' is not assignable to type '["a", "b", bigint]'
*/
_b = _a;
////////////////////////////////////
type Transform<T extends { [tableAlias:string] : { [columnAlias:string] : any } }> = (
{
[tableAlias in keyof T] : (
{
[columnAlias in keyof T[tableAlias]] : (
[tableAlias, columnAlias, T[tableAlias][columnAlias]]
)
}[keyof T[tableAlias]]
)
}[keyof T]
);
interface X<T extends { [tableAlias:string] : { [columnAlias:string] : any } }> {
//This is a function property, not a method
_ : (arg : Transform<T>) => void
}
declare let ax : X<{
a : {
b : bigint,
}
}>;
/**
* ax._ : (arg: ["a", "b", bigint]) => void
* _a._ : (arg: ["a", "b", bigint]) => void
*/
ax._
declare let bx : X<{
a : {
b : bigint|null,
}
}>;
/**
* bx._ : (arg: ["a", "b", bigint | null]) => void
* _b._ : (arg: ["a", "b", bigint | null]) => void
*/
bx._
/**
* Expected : Assignment OK!
* Actual : Assignment not allowed
*
* Type '{ b: bigint | null; }' is not assignable to type '{ b: bigint; }'
*/
ax = bx;
/**
* Assignment not allowed, as expected
*
* Type '["a", "b", bigint | null]' is not assignable to type '["a", "b", bigint]'
*/
bx = ax;
//Assignment OK!
_a = ax;
//Assignment OK!
_a = bx;
/**
* Assignment not allowed, as expected
*
* Type '["a", "b", bigint | null]' is not assignable to type '["a", "b", bigint]'
*/
_b = ax;
//Assignment OK!
_b = bx;
//Assignment OK!
ax = _a;
//Assignment OK!
ax = _b;
/**
* Assignment not allowed, as expected
*
* Type '["a", "b", bigint | null]' is not assignable to type '["a", "b", bigint]'
*/
bx = _a;
//Assignment OK!
bx = _b;
Expected behavior:
This should be allowed,
ax = bx;
Actual behavior:
/**
* Expected : Assignment OK!
* Actual : Assignment not allowed
*
* Type '{ b: bigint | null; }' is not assignable to type '{ b: bigint; }'
*/
ax = bx;
Playground Link: Playground
Related Issues:
I think this might be a duplicate / related to #32311.
Is this the same or a separate issue:
interface Fn<A, B> {
(a: A): B;
done: (fn: (b: B) => void) => Fn<A, void>;
}
declare const f: Fn<string, number>;
const g: Fn<"a", number> = f; // error!?
// Type 'Fn<string, number>' is not assignable to type 'Fn<"a", number>'.
// Type 'string' is not assignable to type '"a"'.(2322)
// ... but why is Fn<A, B> not contravariant in A?
I think that might be separate because the issue you post is probably related to recursive types. My guess as to what is going on there is:
Fn<A, B> observes A as contravariant from the function input.Fn<A, void> has a different type id to Fn<A, B> so it doesn't get related using the recursive assumption. Relating Fn<A, void> to Fn<A, void> (where A is a marker) probable hits the following code path:// When variance information isn't available we default to covariance. This happens
// in the process of computing variance information for recursive types and when
// comparing 'this' type arguments.
A as covariant, and in conjuction with 1, marks A as invariant.Thanks. I wonder if this warrants a brand new issue or should be added as a curiosity onto #1394 for a case where an explicit contravariance annotation would be useful.
I think this warrants a new issue, at least for future referencing. The core problem is that Fn<A, B> gets marked with a special marker flag (to ensure structural checking) but Fn<A, void> does not.
The closest duplicate is probably #33872, but that was a specific regression.
Here's a simpler repro:
// Covariant because T[keyof T] has no alias symbol and we always relate structurally
type A<T> = T[keyof T];
declare let a1: A<{ a: bigint }>;
declare let a2: A<{ a: bigint | null }>;
a1 = a2; // Error
a2 = a1;
// Invariant because of variance measurement for B<T>
type B<T> = { prop: T[keyof T] };
declare let b1: B<{ a: bigint }>;
declare let b2: B<{ a: bigint | null }>;
b1 = b2; // Error
b2 = b1; // Error
We consider T[keyof T] to be invariant in T. Ideally we should consider it contravariant, but as outlined in #32311 there are reasons we can't. So, variance measurements for T end up being too conservative, and T[keyof T] becomes a device for revealing _when_ we perform variance measurements. In the example above, it reveals that we don't measure variance for A<T> because we can't attach a type alias to an indexed access type. However, we do measure variance for B<T> and thus end up being more conservative.
It's a tough problem to solve. If type relations were fully evaluated _structurally_ at all times we'd get the expected results, but we'd also have horrible performance (we've tried, it's not pretty). So, not sure there's much we can do here.
Actually, let me clarify a bit. T[keyof T] is sometimes covariant, sometimes contravariant, and sometimes invariant, depending on how T varies. For example
{ a: string | number } and a subtype { a: string }, T[keyof T] is covariant, but{ a: string } and a subtype { a: string, b: boolean }, T[keyof T] is contravariant, and{ a: string | number } and a subtype { a: string, b: boolean }, T[keyof T] is invariant.So, the only safe thing to assume is that T[keyof T] is invariant, but it may indeed be too conservative at times.
If we could manually annotate type params with a particular variance in the future, would that solve this problem?
Or would we run into cases where it would be too expensive to check that the annotated variance is correct?