Skip to content

Proposal: covariance and contravariance generic type arguments annotations #10717

Closed

Description

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:

  • covariant position, that means for output (such as method/call/construct return types)
  • contravariant position, that means for input (such as input parameters)

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

  • write(x: T): void is removed when T referenced with out
  • read(): T is reset to read(): {} when T referenced with in
  • prop: T becomes readonly prop: T when T referenced with out
  • ... see more details in the proposal document

Examples

Say an interface is defined:

interface A<T> {
    getName(): string;  // no generic parameter referenced
    getNameOf(t: T): string;    // reference in input
    whoseName(name: string): T; // reference in output
    copyFrom(a: A<in T>): void;  // explicitly set contravariance
    copyTo(a: A<out T>): 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<in T> {
    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<in T>): void;  // T is contravariant already
    //copyTo(a: A<out T>): void; // T is covariant, removed
    //current: T;   // T is in bivariant position, write-only could be used if it were supported 
}

interface A<out T> {
    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<in T>): void;  // T is contravariant, removed
    copyTo(a: A<out T>): void;   // T is covariant, left
    readonly current: T;    // readonly property is in covariant position 
}

interface A<in out T> {  // 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<in T>): void;  // T is contravariant, removed
    //copyTo(a: A<out T>): void; // T is covariant, removed
    readonly current: {};    // readonly property is in covariant position, but type is stripped out 
}

Links

Call for people

@ahejlsberg
@RyanCavanaugh
@danquirk

@Aleksey-Bykov
@isiahmeadows

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions