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

So this topic is a discussion point.