Description
openedon Sep 6, 2016
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.
- Strengthen input parameters assignability constraints from considering bivariant to considering contravariant.
- Introduce type variance annotations (
in
andout
) in generic type argument positionsin
annotates contravariant generic type argumentsout
annotates covariant generic type argumentsin out
andout in
annotate bivariant generic type arguments- generic type arguments without these annotations are considered invariant
- The annotated generic types annotated with
in
andout
are internally represented by compiler constructed types (transformation rules are defined in the proposal)
Additionally, there're a few optional modifications being proposed:
- Allow type variance annotation (
in
andout
) in generic type parameter positions to instruct compiler check for co/contravariance violations. - Introduce write-only properties (in addition to read-only), so that contravariant counterpart of read-write property could be extracted
- 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 whenT
referenced without
read(): T
is reset toread(): {}
whenT
referenced within
prop: T
becomesreadonly prop: T
whenT
referenced without
- ... 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
- Original suggestion/discussion Covariance / Contravariance Annotations #1394
- Stricter TypeScript "Stricter" TypeScript #274
- Suggestion to turn off parameter covariance allow a flag that turns off covariant parameters when checking function assignability #6102
Call for people
@ahejlsberg
@RyanCavanaugh
@danquirk
@Aleksey-Bykov
@isiahmeadows