Covariance / Contravariance Annotations · Issue #1394 · microsoft/TypeScript (original) (raw)
(It's a question as well as a suggestion)
Update: a proposal #10717
I've supposed that for structural type system as TypeScript is, type variance isn't applicable since type-compatibility is checked by use.
But when I had read @RyanCavanaugh 's TypeScript 1.4 sneak peek (specifically 'Stricter Generics' section) I realized that there's some lack in this direction by design or implementation.
I wondered this code is compiled:
function push(arr: T[], a: T) { arr.push(a); } var ns = [1]; push<{}>(ns, "a"); // ok, we added string to number[];
More clear code:
interface A { a: number; } interface B extends A { b: number; } interface C extends B { c: number; }
var a: A, b: B, c: C;
var as: A[], bs: B[];
as.push(a); // ok as.push(b); // ok, A is contravariant here (any >=A can be pushed)
bs.push(a); // error, B is contravariant here, so since A<B, A cannot be pushed -- fair bs.push(b); // ok bs.push(c); // ok, C>=B
as = bs; // ok, covariance used?
as.push(a); // ok, but actually we pushed A to B[]
How could B[]
be assignable to A[]
if at least on member push
is not compatible. For B[].push
it expects parameters of type B
, but A[].push
expects A
and it's valid to call it with A
.
To illustrate:
var fa: (a: A) => void; var fb: (b: B) => void;
fa(a); fa(b); fb(a); // error, as expected fa = fb; // no error fa(a); // it's fb(a)
Do I understand it correctly that is by design?
I don't think it can be called type-safe.
Actually, such restriction that could make B[]
to be unassignable to A[]
isn't desirable.
To solve it I suggest to introduce variance on some level (variable/parameter, type?).
Syntax
var as: A[]; // no variance var bs: out B[]; // covariant, for "read" <out A[]>bs; // ok, covariance used <A[]>bs; // same is before, out should be inferred
(<A[]>bs)[0]; // means, allow to get covariant type (<A[]>bs).push(a); // means, disallow to pass covariant type
<in A[]>bs; // fails
function push(data: in T[], val: out T): void { data.push(val); } push(animals, dog); // allowed, T is Animal push(dogs, animal); // disallow, T can't be inferred
// In opposite function find(data: out T[], val: out T): bool { ... } // allow only get from data find(animals, dog); // allowed find(cats, dog); // allowed T can be inferred as Mammal
I'm not sure where variance should be applied - to variable or type?
Looks like it closer to variable it self, so the syntax could be like:
var in a: number[]; function(in a: T[], out b: T): { ... }
Questions to clarification
- inference variance from usage (especially for functions)
- is it applicable for types (like C# uses for interfaces/delegates)
- how to describe variance on member-level (does it need?)
- can variable be
in out
(fixed type)? - can variable be neither
in
norout
(open for upcast/downcast)?
So this topic is a discussion point.