Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional variance annotations #48240

Merged
merged 13 commits into from
Mar 22, 2022
Merged

Optional variance annotations #48240

merged 13 commits into from
Mar 22, 2022

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented Mar 13, 2022

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.

@typescript-bot typescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Mar 13, 2022
@ahejlsberg
Copy link
Member Author

@typescript-bot test this
@typescript-bot user test this inline
@typescript-bot run dt
@typescript-bot perf test faster

@typescript-bot
Copy link
Collaborator

typescript-bot commented Mar 13, 2022

Heya @ahejlsberg, I've started to run the abridged perf test suite on this PR at 3799524. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Mar 13, 2022

Heya @ahejlsberg, I've started to run the parallelized Definitely Typed test suite on this PR at 3799524. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Mar 13, 2022

Heya @ahejlsberg, I've started to run the extended test suite on this PR at 3799524. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Mar 13, 2022

Heya @ahejlsberg, I've started to run the diff-based community code test suite on this PR at 3799524. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

@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 Namets-ci-ubuntu
Platformlinux 4.4.0-210-generic
Architecturex64
Available Memory16 GB
Available Memory4 GB
CPUs4 × Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz
Hosts
  • node (v14.15.1, x64)
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

@typescript-bot
Copy link
Collaborator

@ahejlsberg
Great news! no new errors were found between main..refs/pull/48240/merge

@Andarist
Copy link
Contributor

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.

@Andarist
Copy link
Contributor

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?

@ahejlsberg
Copy link
Member Author

ahejlsberg commented Mar 14, 2022

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.

@RyanCavanaugh
Copy link
Member

Invariant is "eagerly" resolved to its operand, so the variance annotations do not apply.

Invariant2 is an array type, and arrays are always checked covariantly, so the variance annotations do not apply.

We'll be putting this in <h1><blink>....</blink></h1> tags in the blog post and docs: you must write the same variance that would normally be measured. This is not a mechanism for changing the variance of a generic type.

Specifically, you cannot reliably create an invariant type by applying in out to something that's actually covariant or contravariant, and you cannot use ts-ignore to suppress the warning about incorrect variance measurement to reliably turn a contravariant type into a covariant one or vice vesa. Any incoming/outgoing relations to structural forms will still be going through the normal type relational process, so if the annotation is inconsistent with the observed behavior, you will have inconsistent behavior.

@ahejlsberg
Copy link
Member Author

ahejlsberg commented Apr 5, 2022

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.

@Jack-Works
Copy link
Contributor

image

this is interesting

@ariccio
Copy link

ariccio commented Apr 10, 2022

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 😉☺️

@mikearnaldi
Copy link

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

@IllusionMH
Copy link
Contributor

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.

@mikearnaldi
Copy link

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

@RyanCavanaugh
Copy link
Member

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.

@mikearnaldi
Copy link

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

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).
@Jack-Works
Copy link
Contributor

Is TS going to emit variance annotations in .d.ts?

@DanielRosenwasser
Copy link
Member

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

@Jack-Works
Copy link
Contributor

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?

@Andarist
Copy link
Contributor

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?

@weswigham
Copy link
Member

weswigham commented Apr 12, 2022

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.

@Andarist
Copy link
Contributor

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

@benschac
Copy link

@jedwards1211
Copy link

jedwards1211 commented Jun 2, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
Archived in project