Skip to content

Covariance / Contravariance Annotations #1394

Closed
@Igorbek

Description

@Igorbek

(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 nor out (open for upcast/downcast)?

So this topic is a discussion point.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.SuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions