Description
(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<T>(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<T>(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<T>(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<T>(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.