GeezerVison is showing github.com — Optional variance annotations by ahejlsberg · Pull Request #48240 · microsoft/TypeScript
Conversation
With this PR we introduce optional declaration site variance annotations for type parameters of classes, interfaces and type aliases. Annotations take the form of an in and/or out keyword immediately preceding the type parameter name in a type parameter declaration.
- An
out annotation indicates that a type parameter is covariant.
- An
in annotation indicates that a type parameter is contravariant.
- An
in out annotation indicates that a type parameter is invariant.
Generally, type parameter variance is simply a function of how a type parameter is used in its generic subject type. Indeed, when generic type instantiations are related structurally, variance annotations serve no purpose. This is why TypeScript strictly doesn't need variance annotations. However, variance annotations are useful to assert desired type relations of their subject generic types. Specifically, given a generic type G<T> and any two type arguments Super and Sub for which Sub is a subtype of Super,
- if
T is covariant (declared as out T), G<Sub> is a subtype of G<Super>,
- if
T is contravariant (declared as in T), G<Super> is a subtype of G<Sub>, and
- if
T is invariant (declared as in out T), neither G<Super> nor G<Sub> is a subtype of the other.
Intuitively, covariance restricts a type parameter to output (read) positions and contravariance restricts a type parameter to input (write) positions--hence the in and out modifiers. For example:
type Provider<out T> = () => T;
type Consumer<in T> = (x: T) => void;
type Mapper<in T, out U> = (x: T) => U;
type Processor<in out T> = (x: T) => T;
Covariance and contravariance annotations are checked by structurally relating representative instantiations of their subject generic types. For example, in the following generic type, the type parameter T is used in both input and output positions and T is thus invariant. Attempting to mark T covariant
type Foo<out T> = {
x: T;
f: (x: T) => void;
}
reports the following error on out T:
Type 'Foo<sub-T>' is not assignable to type 'Foo<super-T>' as implied by variance annotation.
Types of property 'f' are incompatible.
Type '(x: sub-T) => void' is not assignable to type '(x: super-T) => void'.
Types of parameters 'x' and 'x' are incompatible.
Type 'super-T' is not assignable to type 'sub-T'.
Likewise, attempting to mark T contravariant
type Foo<in T> = {
x: T;
f: (x: T) => void;
}
reports the following error on in T:
Type 'Foo<super-T>' is not assignable to type 'Foo<sub-T>' as implied by variance annotation.
Types of property 'x' are incompatible.
Type 'super-T' is not assignable to type 'sub-T'.
Notice how the error elaborations reveal where and how variance is breached.
Invariance annotations (in out T) are never checked but simply assumed to hold. Thus, it is possible to assert invariance even when the actual usage of a type parameter is co- or contravariant.
When multiple interface declarations are merged, or when a class declaration and one or more interface declarations are merged, variance annotations are aggregated. In the example
interface Bar<T> {
// ...
}
interface Bar<out T> {
// ...
}
interface Bar<in T> {
// ...
}
the aggregate variance of T is in out, and T is thus assumed to be invariant.
When variance annotations are present, the type checker doesn't need to measure variance. Thus, variance annotations can help improve the performance of checking complex and interdependent types. In particular, marking a type parameter invariant means that no measurement or checking is necessary for that type parameter.
In addition, variance annotation can help establish correct variance for multiple circularly dependent generic types. Specifically, when measuring variance, TypeScript limits the structural search space in order to avoid runaway recursion. In the example
type Foo<T> = {
x: T;
f: Bar<T>;
}
type Bar<U> = (x: Baz<U[]>) => void;
type Baz<V> = {
value: Foo<V[]>;
}
declare let foo1: Foo<unknown>;
declare let foo2: Foo<string>;
foo1 = foo2; // Should be an error but isn't
foo2 = foo1; // Error
the compiler measures T to be covariant even though it is actually invariant due to variance reversal in Bar and the circular reference in Baz. The compiler could establish that by continuing to structurally relating nested circular references until some fixed point, but this gets exponentially expensive and isn't feasible in complex scenarios. Adding an in out annotation to T establishes the correct variance and produces the expected errors.
We're marking #1394 and #10717 fixed by this PR, although the feature implemented isn't exactly what is suggested in those issues.
Fixes #1394.
Fixes #10717.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
IllusionMH, r-cyr, whzx5byb, a-eid, sledorze, sinclairzx81, davojan, acid-chicken, linonetwo, RyanCavanaugh, and 52 more reacted with thumbs up emoji
benschac, worstpractice, and fonesti reacted with laugh emoji
suchipi, fvilante, benschac, niieani, Adjective-Object, jcalz, mkosir, nokazn, anuraghazra, emily-curry, and 9 more reacted with hooray emoji
alexrock, r-cyr, pfgray, whzx5byb, a-eid, sledorze, TarikGul, belgattitude, minedeljkovic, tsofist, and 24 more reacted with heart emoji
whzx5byb, harrysolovay, a-eid, sledorze, Igorbek, fvilante, benschac, niieani, michal-minich, mkosir, and 12 more reacted with rocket emoji
whzx5byb, tonivj5, nnnnoel, a-eid, sledorze, nirtamir2, rtrembecky, fvilante, benschac, ArnaudBarre, and 6 more reacted with eyes emoji
All reactions
-
👍
62 reactions
-
😄
3 reactions
-
🎉
19 reactions
-
❤️
34 reactions
-
🚀
22 reactions
-
👀
16 reactions
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Anders Hejlsberg (@ahejlsberg)
The results of the perf run you requested are in!
Here they are:
Comparison Report - main..48240
| Metric |
main |
48240 |
Delta |
Best |
Worst |
| Angular - node (v14.15.1, x64) |
| Memory used |
332,434k (± 0.01%) |
332,458k (± 0.00%) |
+23k (+ 0.01%) |
332,425k |
332,481k |
| Parse Time |
2.03s (± 0.68%) |
2.03s (± 0.55%) |
-0.00s (- 0.25%) |
2.01s |
2.05s |
| Bind Time |
0.87s (± 0.55%) |
0.87s (± 0.64%) |
+0.00s (+ 0.12%) |
0.85s |
0.88s |
| Check Time |
5.59s (± 0.50%) |
5.59s (± 0.36%) |
+0.01s (+ 0.13%) |
5.55s |
5.64s |
| Emit Time |
6.29s (± 0.66%) |
6.27s (± 0.33%) |
-0.03s (- 0.44%) |
6.23s |
6.32s |
| Total Time |
14.78s (± 0.40%) |
14.75s (± 0.23%) |
-0.03s (- 0.18%) |
14.67s |
14.82s |
| Compiler-Unions - node (v14.15.1, x64) |
| Memory used |
193,349k (± 0.68%) |
193,474k (± 0.60%) |
+126k (+ 0.07%) |
191,863k |
195,183k |
| Parse Time |
0.86s (± 0.68%) |
0.86s (± 0.80%) |
-0.00s (- 0.12%) |
0.84s |
0.87s |
| Bind Time |
0.55s (± 0.67%) |
0.56s (± 0.53%) |
+0.00s (+ 0.54%) |
0.55s |
0.56s |
| Check Time |
7.43s (± 0.86%) |
7.42s (± 0.60%) |
-0.02s (- 0.22%) |
7.31s |
7.50s |
| Emit Time |
2.51s (± 1.48%) |
2.52s (± 2.05%) |
+0.01s (+ 0.44%) |
2.45s |
2.69s |
| Total Time |
11.35s (± 0.77%) |
11.35s (± 0.73%) |
-0.00s (- 0.03%) |
11.23s |
11.58s |
| Monaco - node (v14.15.1, x64) |
| Memory used |
325,407k (± 0.01%) |
325,416k (± 0.00%) |
+9k (+ 0.00%) |
325,376k |
325,449k |
| Parse Time |
1.57s (± 0.46%) |
1.57s (± 0.68%) |
-0.00s (- 0.06%) |
1.56s |
1.61s |
| Bind Time |
0.78s (± 0.98%) |
0.78s (± 0.61%) |
+0.00s (+ 0.26%) |
0.76s |
0.78s |
| Check Time |
5.46s (± 0.31%) |
5.49s (± 0.45%) |
+0.03s (+ 0.59%) |
5.44s |
5.54s |
| Emit Time |
3.32s (± 0.49%) |
3.31s (± 0.51%) |
-0.01s (- 0.27%) |
3.27s |
3.35s |
| Total Time |
11.12s (± 0.22%) |
11.14s (± 0.27%) |
+0.02s (+ 0.22%) |
11.07s |
11.19s |
| TFS - node (v14.15.1, x64) |
| Memory used |
288,853k (± 0.01%) |
288,847k (± 0.01%) |
-6k (- 0.00%) |
288,814k |
288,894k |
| Parse Time |
1.31s (± 1.46%) |
1.31s (± 1.25%) |
-0.00s (- 0.23%) |
1.28s |
1.35s |
| Bind Time |
0.73s (± 0.79%) |
0.74s (± 0.88%) |
+0.01s (+ 1.10%) |
0.73s |
0.75s |
| Check Time |
5.12s (± 0.56%) |
5.12s (± 0.64%) |
-0.00s (- 0.08%) |
5.06s |
5.20s |
| Emit Time |
3.50s (± 2.10%) |
3.52s (± 1.78%) |
+0.02s (+ 0.63%) |
3.40s |
3.62s |
| Total Time |
10.67s (± 0.98%) |
10.69s (± 0.55%) |
+0.02s (+ 0.18%) |
10.58s |
10.82s |
| material-ui - node (v14.15.1, x64) |
| Memory used |
453,748k (± 0.01%) |
453,653k (± 0.01%) |
-95k (- 0.02%) |
453,578k |
453,702k |
| Parse Time |
1.85s (± 0.63%) |
1.85s (± 0.50%) |
+0.00s (+ 0.05%) |
1.83s |
1.87s |
| Bind Time |
0.70s (± 0.84%) |
0.71s (± 0.70%) |
+0.00s (+ 0.28%) |
0.69s |
0.71s |
| Check Time |
20.61s (± 0.86%) |
20.40s (± 0.68%) |
-0.22s (- 1.05%) |
20.24s |
20.93s |
| Emit Time |
0.00s (± 0.00%) |
0.00s (± 0.00%) |
0.00s ( NaN%) |
0.00s |
0.00s |
| Total Time |
23.17s (± 0.74%) |
22.96s (± 0.61%) |
-0.21s (- 0.91%) |
22.80s |
23.50s |
| xstate - node (v14.15.1, x64) |
| Memory used |
535,121k (± 0.01%) |
535,273k (± 0.01%) |
+152k (+ 0.03%) |
535,202k |
535,328k |
| Parse Time |
2.57s (± 0.51%) |
2.56s (± 0.39%) |
-0.00s (- 0.00%) |
2.54s |
2.59s |
| Bind Time |
1.15s (± 0.78%) |
1.15s (± 0.77%) |
+0.00s (+ 0.09%) |
1.13s |
1.16s |
| Check Time |
1.50s (± 0.53%) |
1.50s (± 0.75%) |
-0.00s (- 0.07%) |
1.48s |
1.52s |
| Emit Time |
0.07s (± 0.00%) |
0.07s (± 0.00%) |
0.00s ( 0.00%) |
0.07s |
0.07s |
| Total Time |
5.28s (± 0.42%) |
5.30s (± 0.30%) |
+0.01s (+ 0.25%) |
5.25s |
5.32s |
System
| Machine Name | ts-ci-ubuntu |
| Platform | linux 4.4.0-210-generic |
| Architecture | x64 |
| Available Memory | 16 GB |
| Available Memory | 4 GB |
| CPUs | 4 × Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz |
Scenarios
- Angular - node (v14.15.1, x64)
- Compiler-Unions - node (v14.15.1, x64)
- Monaco - node (v14.15.1, x64)
- TFS - node (v14.15.1, x64)
- material-ui - node (v14.15.1, x64)
- xstate - node (v14.15.1, x64)
| Benchmark |
Name |
Iterations |
| Current |
48240 |
10 |
| Baseline |
main |
10 |
Developer Information:Download Benchmark
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Mar 13, 2022
|
In here it has been mentioned that:
For now, we'll have to accept that variance measurement for circular types is sometimes inaccurate and/or dependent on declaration order
So from what I get - with this PR here we are going to have a somewhat unpredictable behavior by default and to make it predictable we'll be able to use variance annotations introduced by this PR. Couldn't/shouldn't the problem be flipped though? Couldn't you go with predictable and safe behavior (although slow) and propose using those variance annotations as performance optimization?
I understand that some libraries out there might be hard/slow to upgrade and to start using those annotations but with the time that should become less of a concern and people will notice great slowdowns pretty soon and they should start helping library authors to migrate to this performance optimization. With typesVersions it should be possible to support older TS versions while introducing those perf hints for 4.7.
I'm worried that by accepting the random behavior now the community will get stuck with it forever - while if this would be treated as a perf optimization then most people would get great safety improvements immediately and out of the box.
|
ctjlewis reacted with heart emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Correct me if Im wrong but in here:
Likewise, attempting to mark T invariant
It should be “contravariant”
When variance annotations are present, the type checker doesn't need to measure variance.
hm, how measuring is different from what u check and for what u raise errors described in the thread? Since reporting “correct” errors would require full structural computation - shouldnt this be described here as a limitation for raising those errors? I mean - sometimes they just wont be raised and u will trust the user input, did i get this right?
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Couldn't you go with predictable and safe behavior (although slow)
We considered it and we don't think it's feasible. An unknown number of code bases out there would be negatively impacted, in some cases quite dramatically. To wit, compilation of immutable.js slowed down 50x with the candidate PR. It's an awful experience when updating to the latest version of TypeScript all of a sudden hangs your build process. I'd love to do better, but for the moment we don't have other candidate solutions.
Also, it really is relatively rare that the current implementation gets variance measurement wrong. It requires types to be circularly dependent and to flip variance somewhere in the circularity. That happens, but it definitely isn't common.
I'm worried that by accepting the random behavior now the community will get stuck with it forever
The scenarios in which the issue occurs are rare and well understood, and we'll continue to think about ways in which we can improve accuracy of variance measurement without adversely affecting performance. Nothing prevents us from adopting a better algorithm in the future.
It should be “contravariant”
Indeed. Thanks for catching that, I have fixed it.
how measuring is different from what u check and for what u raise errors described in the thread?
Variance measurement is similar to variance annotation checking, but requires more checks. Specifically, we always perform at least two checks (subtype-to-supertype and supertype-to-subtype) to measure variance, and sometimes require a third check to distinguish bivariance from unwitnessed type parameters. In contrast, annotation checking requires only one check for co- or contravariance, and no check at all for invariance.
|
c69-addepar reacted with heart emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
It seems variance annotations are ignored even when checking the same generic type
Yeah, variance annotations are only consulted when relating instantiations of type aliases for object, function, and constructor type literals and mapped types, so it is kind of meaningless to add variances annotations for other kinds of types. I think the best course of action is for us to error on attempts to do so. I will put up a PR to that effect.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|

this is interesting
|
wdoug, Beraliv, and Mayandev reacted with eyes emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
I'm just dropping by to wonder if whoever came up with this was a C programmer back in the day who loved SAL? Because I sense a bit of SAL here 😉☺️
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
It seems variance annotations are ignored even when checking the same generic type
Yeah, variance annotations are only consulted when relating instantiations of type aliases for object, function, and constructor type literals and mapped types, so it is kind of meaningless to add variances annotations for other kinds of types. I think the best course of action is for us to error on attempts to do so. I will put up a PR to that effect.
Checking after diagnostics have been added it seems that it is still possible to define a parameter invariant even if the measured variance is either covariant or contravariant. Is this intended? It seems to me that given annotations are ignored in structural comparisons the only thing that make sense is to have them being strictly equal to the measured one
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
it is still possible to define a parameter invariant even if the measured variance is either covariant or contravariant. Is this intended?
Yes it's explicitly stated in OP
Invariance annotations (in out T) are never checked but simply assumed to hold. Thus, it is possible to assert invariance even when the actual usage of a type parameter is co- or contravariant.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
it is still possible to define a parameter invariant even if the measured variance is either covariant or contravariant. Is this intended?
Yes it's explicitly stated in OP
Invariance annotations (in out T) are never checked but simply assumed to hold. Thus, it is possible to assert invariance even when the actual usage of a type parameter is co- or contravariant.
Yes, I am asking if it is still intended and an explanation of why that is ok
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Yes, I am asking if it is still intended and an explanation of why that is ok
Yes. Part of the goal here is not just correctness, but performance. For many libraries causing slowdowns during typing (as in, typing in the editor), variance measurement accounts for a very large portion of the CPU time spent, so "skipping" checks when the author tells us what the result is going to be is a big performance win. Asserting invariance specifically means there's no possible soundness gap created as a result, since only fewer relations are allowed as a result.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Yes, I am asking if it is still intended and an explanation of why that is ok
Yes. Part of the goal here is not just correctness, but performance. For many libraries causing slowdowns during typing (as in, typing in the editor), variance measurement accounts for a very large portion of the CPU time spent, so "skipping" checks when the author tells us what the result is going to be is a big performance win. Asserting invariance specifically means there's no possible soundness gap created as a result, since only fewer relations are allowed as a result.
Thanks! I have definitely felt the pain in some scenarios, I agree there is no soundness gap from the structural perspective, there may be a perceived soundness gap when those annotations are ignored in structural fallback if there is a mismatch between the annotated variance and the actual variance but if the library authors are aware of it perf wins
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
David Glasser (glasser)
added a commit
to apollographql/apollo-server
that referenced
this pull request
Apr 11, 2022
Two things got a bit mixed together here.
One is that we want the Disabled plugins to all be defined directly in
plugin/index.ts, so that loading ApolloServerPluginInlineTraceDisabled
does not spend time loading the protobuf library.
The other thing is that we started thinking a bit more carefully about
plugin context generics. We wrote some tests to make sure that you can
use an `ApolloServerPlugin<BaseContext>` (ie, a plugin that doesn't need
any particular fields on the context) with any `ApolloServer`, but not
vice versa. As part of getting this to work, we added another
`__forceTContextToBeContravariant`. We also noticed that it made more
sense for BaseContext to be `{}` ("an object with no particular fields")
rather than `Record<string, any>` ("an object where you can do anything
with any of its fields").
We investigated whether the new contravariance annotation coming in the
next two months in TS 4.7
(microsoft/TypeScript#48240) would allow us to
get rid of the `__forceTContextToBeContravariant` hack and the answer is
yes! However, trying `4.7.0-beta` failed for two other reasons. One is
that microsoft/TypeScript#48366 required us to
add some missing `extends` clauses, which we are doing now in this PR.
The other is that `graphql-tools` needs some fixes to work with TS4.7
which we hope they can do soon
(ardatan/graphql-tools#4381).
|
Is TS going to emit variance annotations in .d.ts?
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
At the moment, the intent is to emit whatever you've written. If you leave a type parameter bare, we'll continue to leave it bare, which will mean that consuming modules will continue to infer the variance.
If you need to provide variance annotations for newer users, but need to provide an experience that gracefully degrades, you can use
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
At the moment, the intent is to emit whatever you've written.
Can I add a compiler flag or something to let typescript emit those variance annotations in .d.ts so it will automatically speed up the compilation?
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Can I add a compiler flag or something to let typescript emit those variance annotations in .d.ts so it will automatically speed up the compilation?
Do you mean emitting the computed variance annotations?
|
Jack-Works reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
The computed variances are more often than not much more complicated than the simple in/out/in out the annotations expose (mapped types and conditional types have odd assignability relationships!), and since writing a variance annotation that doesn't exactly match the measured variance can lead to surprising behavior, we leave it to users to annotate as they see necessary, and make that tradeoff consciously.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
mapped types and conditional types have odd assignability relationships!
Would love to read a write-up about this! It could demystify some bits about the TS compiler for a lot of people
|
tonivj5, A-Babin, Beraliv, 1valdis, and millsp reacted with thumbs up emoji
tonivj5 reacted with eyes emoji
All reactions
-
👍
5 reactions
-
👀
1 reaction
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Should using the covariance out keyword to make the generic assigned to a property readonly?
Example:
a is covariant and cannot be assigned to a number, or anything.
a can be assigned to a new value.
a can be assigned to a new value.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
I just realized, there's no way to mark a parameter bivariant to skip variance measurements on types with bivariant parameters. Is there a fundamental reason variance measurement can't be avoided when there's a bivariant parameter, or would explicit variance annotations be a reasonable feature request?
Maybe I misunderstand but it seems like the only hope of reliably avoiding potentially infinite type errors with complex circular types is to explicitly annotate every type parameter so that TS won't perform variance measurement and we know there's no risk of accidentally crossing the threshold with slightly more complex types. So I wish I could also explicitly annotate bivariance.
I'm pretty sure I wasn't thinking and parameters aren't allowed to be bivariant...I was experimenting with adding variance annotations to ZodObject and have errors making the shape parameter parameter covariant, contravariant, and invariant, so I mistakenly assumed it needs to be bivariant but I must have needed to fix something else.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
|