Repository navigation
Proposal: covariance and contravariance generic type arguments annotations #10717
Description
Activity
This reminds me a lot of Kotlin's covariant and contravariant generics
syntactically. Just a first impression (I haven't really dug deep into this
yet).On Mon, Sep 5, 2016, 23:23 Igor Oleinikov notifications@github.com wrote:
I have published a proposal document that makes attempt to address an
outstanding issue with type variance, that was brought and discussed at
#1394 #1394The 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
ProblemThere'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 errorCurrently, TypeScript considers input parameters bivariant
https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/Type%20Compatibility.md#function-parameter-bivariance
.
That's been designed in that way to avoid too strict assignability rules
that would make language use much harder. Please see links section
<#m_-8167296998011622822_links> for argumentation from TypeScript team.There're more problematic examples at the original discussion #1394
#1394
Proposal summaryPlease see proposal document
https://github.com/Igorbek/TypeScript-proposals/blob/covariance/covariance/proposal.md
for details.- Strengthen input parameters assignability constraints from
considering bivariant to considering contravariant. - Introduce type variance annotations (in and out) in generic type
argument positions- in annotates contravariant generic type arguments
- out annotates covariant generic type arguments
- in out and out in annotate bivariant generic type arguments
- generic type arguments without these annotations are considered
invariant - 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:
- Allow type variance annotation (in and out) 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 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 {
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 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
}Links
- Original suggestion/discussion Covariance / Contravariance Annotations #1394
Covariance / Contravariance Annotations #1394 - Stricter TypeScript "Stricter" TypeScript #274
"Stricter" TypeScript #274 - Suggestion to turn off parameter covariance allow a flag that turns off covariant parameters when checking function assignability #6102
allow a flag that turns off covariant parameters when checking function assignability #6102
Call for people
Anders Hejlsberg (@ahejlsberg) https://github.com/ahejlsberg
Ryan Cavanaugh (@RyanCavanaugh) https://github.com/RyanCavanaugh
Dan Quirk (@danquirk) https://github.com/danquirkAleksey-Bykov https://github.com/aleksey-bykov
@isiahmeadows https://github.com/isiahmeadows—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#10717, or mute the thread
https://github.com/notifications/unsubscribe-auth/AERrBPCIezFYgMMrjMLcPWA5UDWKa9hZks5qnNy-gaJpZM4J1bdK
.- Strengthen input parameters assignability constraints from
DanielRosenwasser commented
on Sep 6, 2016 MemberMore actions@isiahmeadows yup, it's called use-site variance. Kotlin has both use- and declaration- site variance. Check out their paper on it.
I can't say whether we're committed to variance, but I highly suspect that given the way
Arrayis used, use-site variance would be necessary; that's at least my at-a-glance opinion. The bigger problem as I see it is how variance would be inferred. Clearly you wouldn't want to make people write out types more often for this, so inferring would require some more machinery to work well.Reacted by Herrington Darkholme, Max Bo and Andrei VinersanIgorbek commented
on Sep 6, 2016 ContributorAuthorMore actionsIn my proposal I'm also pointing declaration-site variance as optional part. It must just instruct compiler to verify for variance violations in type definitions, such as you cannot have covariant type taken in contravariant position.
Reacted by Herrington Darkholme and Andrei VinersanIgorbek commented
on Sep 6, 2016 ContributorAuthorMore actionsDaniel Rosenwasser (@DanielRosenwasser) if the inferring is too complicated, it might be a work for tooling as a first stage. Type argument without
inandoutis naturally understood as an invariant, so if we infer variance, we'd need to have a way to specify invariants explicitly (inv,invariant,exact).Yep. That's a good reason to need it.
Oh, and given the above, I think the default behavior should be changed
after this gets implemented.On Tue, Sep 13, 2016, 11:47 Aaron Lahman notifications@github.com wrote:
Please fight for this feature!
One very serious limitation of the assignable to-or-from rule in 3.11.2 is
that Promises are unsafe. Consider the following codevar p: Promise = ... ;
var p2: Promise<{}>;
var p3; Promise;
p2 = p;
p3 = p2;
p3.then(s => s.substr(2)); // runtime error, no method 'substr' on numberThis code shouldn't typecheck. Assigning {} to string would not
typecheck. However, because of function arguments bivariance, Promise<{}>
assigns to Promise.—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#10717 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AERrBD2yh8h_mK90nMZPDCV8cX0Wo6cfks5qpsWdgaJpZM4J1bdK
.Good point. And of course, if you want to change the default, you have to
put an option there first (they added one for--strictNullChecks)On Tue, Sep 13, 2016, 12:07 Aaron Lahman notifications@github.com wrote:
@isiahmeadows https://github.com/isiahmeadows I doubt the default will
get changed. I messaged the Typescript team back in 2013 about this -- back
in 2013, promises were still pretty rare things. They agreed that promises
would be important, but for every example I could show that broke, they
could find 100 examples of jQuery and other frameworks that would have to
forego all type checking if they changed the default.However, a lot has changed since 2013. Typescript 2.0 has shiny new type
checker options. Maybe there's room in a future release to add an option
for soundness.—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#10717 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AERrBOnTJXbgq_Whb3T2kX-wdNhP76rkks5qpsj2gaJpZM4J1bdK
.Igorbek commented
on Sep 13, 2016 ContributorAuthorMore actions@aaronla-ms @isiahmeadows appreciate your support guys! I'm definitely gonna fight for this.
For me it seems pretty clear that current workaround where parameters are bivariant is not playing well with type safety. However I understand the TypeScript team when they're saying about complexity that could be introduced if just enforce users to write annotations always. So to keep language usage simple, inferring system must be smart enough so that means its implementation can really be challenging.
Probably we could investigate when declaration-site annotations (which is cheap) and simple inference rules solve majority of real-world issues and where use-site annotations would be really necessary.Issue with promises could be solved with declaration-site variance. Let's imaging how would
Promisedefinition look like:interface Promise<out T> { // declaration-site covariance that enforces interface verification then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => TResult | PromiseLike<TResult>): PromiseLike<TResult>; then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => void): PromiseLike<TResult>; } var p: Promise</*implicitly out */ number> = ... ; var p2: Promise<{}>; var p3; Promise<string>; p2 = p; // ok p3 = p2; // here an error would be cought p3.then(s => s.substr(2)); // runtime error, no method 'substr' on number
Igorbek commented
on Sep 13, 2016 ContributorAuthorMore actionsI'm assuming you mean with declaration-site variance and parameter contravariance
Yes, that is exactly what I meant.
TypeScript, if I understand correctly, already has parameter contravariance
support (U extends TwhereTis a class type parameter), which Promises
need. They need the covariantU super Tfor the other direction, though,
for completeness.On Tue, Sep 13, 2016, 16:47 Igor Oleinikov notifications@github.com wrote:
I'm assuming you mean with declaration-site variance and parameter
contravarianceYes, that is exactly what I meant.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#10717 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AERrBDxCLqg7FTut9bYh4BGkQvlaQS4zks5qpwvFgaJpZM4J1bdK
.Reacted by Weston PaceThe current
Promisedefinition allows assigning across types even without the bivariance issue. See #9953 and #10524.The following simplification of @aaronla-ms's example still compiles fine (without bivariance):
var p: Promise<number> = ... ; var p3: Promise<string>; p3 = p; p3.then(s => s.substr(2)); // runtime error, no method 'substr' on number
Igorbek commented
on Sep 14, 2016 ContributorAuthorMore actions@isiahmeadows that is not quite the same. you're pointing to type argument constrains (
superconstraint suggestion is tracked in #7004 and #7265) which are orthogonal to type variance. Constraints set relations between different type parameters (say<T, U>whereT extends U). Variance sets relations between same type parameters in variations of produced existential (with concrete type arguments) types (whenTis subtype ofUthenX<out T>is subtype ofX<out U>).Igorbek commented
on Sep 14, 2016 ContributorAuthorMore actionsTroy Gerwien (@yortus) that issue is caused by exact same issue - parameter bivariance. If promise had a property of type
T(likewise C#'sTask<T>.Result) then it would be covariant. But sincePromiseexposes its underlying typeTwithin parameter position it becomes bivariant.43 remaining items
React's contravariant
refcould be solved just by making that property write-only, if only that existed.Reacted by quanganhtran, Sébastien Lorber and Luke Deen TaylorI think I'm bumping into this when using slotted components in react?
Say for example I have some component
<HostComponent<T> renderView={ViewRenderer<T>} />
where
renderViewis used to render some subcomponent ofHostComponent<HostComponent<Chicken> renderView={BirdView} />
My instinct is that this should typecheck since ViewRenderer only reads T to render a component, but it doesn't in current typescript since
ViewRenderer<T>is covariant on T instead of contravariant.
Edit: for anyone else looking at a similar problem, the solution I came to was to reassign the identifiers through a utility type. I'm not happy with the solution, but at least it'll throw an error at the callsite if the types change in the future.
/** * Because typescript doesn't support contravariance or writeonly props to components, * 'write-only' parameter (e.g. generic component slots) must be cast to exact types at * the callsite. * * See related typescript issue * https://github.com/Microsoft/TypeScript/issues/10717 * * This alias checks that the type we're casting to is a subtype of the exact expected type * so the cast site won't break silently in the future. */ type VarianceHack<ParentType, ChildType> = ChildType extends ParentType ? ParentType : never; const ChickenView = BirdView as VarianceHack< ViewRenderer<Chicken>, typeof BirdView >; ///... <HostComponent<Chicken> renderView={ChickenView} />
RyanCavanaugh commented
on Aug 19, 2019 MemberMore actionsI came back to this issue because we've been seeing a lot of confusion around how variance gets measured. A particular example is something that looks covariant:
class Reader<T> { value!: T; getProperty(k: keyof T): T[keyof T] { return this.value[k]; } } type A = { a: string }; type AB = A & { b: number }; function fn<T>(inst: Reader<A>) { const s: string = inst.getProperty("a"); } declare const ab: Reader<AB>; // Disallowed, why? fn(ab);
It really seems like
Reader<T>is covariant overT, and it is as long asknever originates in an aliasedkeyof Tposition. You have to add a field somewhere but it doesn't modify the class variance:class Reader<T> { value!: T; someKey!: keyof T; getProperty(k: keyof T): T[keyof T] { return this.value[k]; } } type A = { a: string }; type AB = A & { b: number }; function fn<T>(inst: Reader<A>) { const s: string = inst.getProperty(inst.someKey); } declare const ab: Reader<AB>; // Legal ab.someKey = "b"; // Causes s: string to get a number fn(ab);
Indeed if you just extracted out
getPropertyto a bare function, it'd be obviously contravariant:declare const a: A; declare const kab: keyof AB; declare function getProperty<T, K extends keyof T>(value: T, key: K): T[K]; // Illegal because kab could be 'b' getProperty(a, kab);
The problem is the original example here is not really contravariant/invariant without some aliasing step, and you have no way to assert that this aliasing doesn't actually occur in your program - the measured variance is the measured variance, full stop.
The follow-on is that it's not clear what to do. If you let you write
class Reader<covariant T> {
we'd presumably just have to error on the declaration of
getProperty, because it really is not a covariant usage - in any reasonable definition, this would work the same wayimplementsdoes (an assertion of a measured fact, not an override of reality).It seems like what you want is some way to annotate specific use sites of
Tto override their variance measurement, or maybe some crazy way to definegetPropertyin a way that it disallows aliased values ofk, though it's not clear how that's even remotely possible.As for use-site variance annotations, I don't think this is a good route. The variance of any particular site is purely manifested by its position; the only real missing feature here is
writeonly(which I think is ultimately inevitable, despite our protests).Reacted by SlurpTheo and Anatoly RessinIgorbek commented
on Aug 20, 2019 ContributorAuthorMore actionsRyan Cavanaugh (@RyanCavanaugh) thank you for getting back to this issue and keeping thinking of attacking it in some direction.
I totally agree that this a common confusion what is variance is and how it relates to readonly-ness. It seems to me that most of the time covariance and readonly-ness are used interchangeably.
However, I would argue that in my original proposal I had the same misunderstanding and, more importantly, I still insist that use-site variance has its own dedicated value for the type system correctness and expressiveness.
First, I want to admit that having such a level of expressiveness definitely requires a very advanced understanding of it is and how to use it correctly. It is supposed to be used by library and type definition authors. Therefore, I agree the feature needs to be designed very carefully to not harm most regular users. No rush at all, especially many use-cases were already addressed with
readonlyand stricter variance checking.As for use-site variance annotations, I don't think this is a good route. The variance of any particular site is purely manifested by its position; <...>
There're still positions where the variance in respect to generic types cannot be manifested by its position. A simple example can show this:
/** Moves data from the source array to the destination array and return the destination's final size */ function moveData<T>(source: readonly T[], destination: writeonly T[]): number { while (source.length) { destination.push(source.pop()); // 'pop' is not defined on ReadonlyArray<T> } return destination.length; // 'length' getter is not defined on WriteonlyArray<T> }
What the intent there really is not readonly/writeonly for these arrays. Is that a guarantee that only subtypes of
Twill be used fromsourceand supertypes ofTindestination:moveData(cats, animals); // allowed moveData(animals, cats); // disallowed
The correct signature to express that would be:
declare function moveData<T>( source: { readonly length: number; pop(): T; }, // T is covariant destination: { readonly length: number; push(item: T): void; } // T is contravariant ): number;
So, what I've been suggesting, is to allow use-site variance annotations that construct new types from existing:
declare function moveData<T>( source: out T[], // not the same as readonly T[] destination: in T[] // not the same as writeonly T[] ): number;
The examples with arrays usually are very confusing because they are usually really meant readonly/writeonly. But I wanted to show with simple constructs.
Reacted by Kasper Isager Dalsgarð, Claudia Meadows, Russell Davis, RV, Flavio Vilante, Jacob Mathew, David, Anatoly Ressin, 8bitcrag, Jason Kuhrt and 2 moreJust hit the exact case Jessica Franco (@Jessidhia) mentioned in #10717 (comment).
Is there a workaround other than
// @ts-ignoreavailable?- added 3 commits that reference this issue
on May 14, 2021 I've had to build up a type-level DSL around a type-level map to register variance for given data types. I've been especially interested in utilizing Contravariance to build intersections of requirements from smaller pieces fwiw. I think it'd be a great step in the right direction to be able to specify variance per generic explicitly. Any future plans to revisit this issue?
Reacted by Dmytro Yaremenko, Didier Villevalois, Flavio Vilante and whzx5bybI would like the variance to be untied from generics, but to be a property of types and could be derived from the function code. Look at some analysis here: https://dev.to/ninjin/variance-from-zero-to-the-root-57ek
- added 2 commits that reference this issue
on Jul 21, 2024
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:
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.
inandout) in generic type argument positionsinannotates contravariant generic type argumentsoutannotates covariant generic type argumentsin outandout inannotate bivariant generic type argumentsinandoutare internally represented by compiler constructed types (transformation rules are defined in the proposal)Additionally, there're a few optional modifications being proposed:
inandout) in generic type parameter positions to instruct compiler check for co/contravariance violations.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:
write(x: T): voidis removed whenTreferenced withoutread(): Tis reset toread(): {}whenTreferenced withinprop: Tbecomesreadonly prop: TwhenTreferenced withoutExamples
Say an interface is defined:
So that, when it's referenced as
A<out T>or with any other annotations, the following types are actually constructed and used:Links
Call for people
Anders Hejlsberg (@ahejlsberg)
Ryan Cavanaugh (@RyanCavanaugh)
Dan Quirk (@danquirk)
Aleksey-Bykov
@isiahmeadows