Proposal: covariance and contravariance generic type arguments annotations · Issue #10717 · microsoft/TypeScript (original) (raw)

I have published a proposal document that makes attempt to address an outstanding issue with type variance, that was brought and discussed at #1394

The work is currently not complete, however the idea is understood and just needs proper wording and documenting. I would like to hear feedback from the TypeScript team and community before I waste too much :).

Please see the proposal here - https://github.com/Igorbek/TypeScript-proposals/tree/covariance/covariance, and below is a summary of the idea

Problem

There's a huge hole in the type system that assignability checking does not respect a contravariant nature of function input parameters:

class Base { public a; } class Derived extends Base { public b; }

function useDerived(derived: Derived) { derived.b; }

const useBase: (base: Base) => void = useDerived; // this must not be allowed useBase(new Base()); // no compile error, runtime error

Currently, TypeScript considers input parameters bivariant.
That's been designed in that way to avoid too strict assignability rules that would make language use much harder. Please see links section for argumentation from TypeScript team.

There're more problematic examples at the original discussion #1394

Proposal summary

Please see proposal document for details.

  1. Strengthen input parameters assignability constraints from considering bivariant to considering contravariant.
  2. Introduce type variance annotations (in and out) in generic type argument positions
    1. in annotates contravariant generic type arguments
    2. out annotates covariant generic type arguments
    3. in out and out in annotate bivariant generic type arguments
    4. generic type arguments without these annotations are considered invariant
  3. The annotated generic types annotated with in and out are internally represented by compiler constructed types (transformation rules are defined in the proposal)

Additionally, there're a few optional modifications being proposed:

  1. Allow type variance annotation (in and out) in generic type parameter positions to instruct compiler check for co/contravariance violations.
  2. Introduce write-only properties (in addition to read-only), so that contravariant counterpart of read-write property could be extracted
  3. Improve type inference system to make possible automatically infer type variance from usage

Details

Within a type definitions each type reference position can be considered as:

So that when a generic type referenced with annotated type argument, a new type constructed from the original by stripping out any variance incompatibilities:

Examples

Say an interface is defined:

interface A { getName(): string; // no generic parameter referenced getNameOf(t: T): string; // reference in input whoseName(name: string): T; // reference in output copyFrom(a: A): void; // explicitly set contravariance copyTo(a: A): void; // explicitly set covariance current: T; // read-write property, both input and output }

So that, when it's referenced as A<out T> or with any other annotations, the following types are actually constructed and used:

interface A { getName(): string; // left untouched getNameOf(t: T): string; // T is in contravariant position, left whoseName(name: string): {}; // T is in covariant position, reset to {} copyFrom(a: A): void; // T is contravariant already //copyTo(a: A): void; // T is covariant, removed //current: T; // T is in bivariant position, write-only could be used if it were supported }

interface A { getName(): string; // left untouched //getNameOf(t: T): string; // T is in contravariant position, removed whoseName(name: string): T; // T is in covariant position, left //copyFrom(a: A): void; // T is contravariant, removed copyTo(a: A): void; // T is covariant, left readonly current: T; // readonly property is in covariant position }

interface A { // bivariant getName(): string; // left untouched //getNameOf(t: T): string; // T is in contravariant position, removed whoseName(name: string): {}; // T is in covariant position, reset to {} //copyFrom(a: A): void; // T is contravariant, removed //copyTo(a: A): void; // T is covariant, removed readonly current: {}; // readonly property is in covariant position, but type is stripped out }

Call for people

@ahejlsberg
@RyanCavanaugh
@danquirk

@Aleksey-Bykov
@isiahmeadows