TypeScript: Variance Annotations do not impact "infer" correctly
đ Search Terms
Variance Annotations, infer, extends, type
đ Version & Regression Information
- This is the behavior in every version I tried (4.7 +), and I reviewed the FAQ for entries about âbugs-that-arent-bugsâ
⯠Playground Link
đ» Code
interface Brand<in out T> {}
type Result = ({unrelatedField: number} & Brand<string>) extends Brand<infer T> ? T : never;
đ Actual behavior
Result
is unknown
This is incorrect since the inferred type parameter is âstringâ not âunknownâ. Additionally, âunknownâ is not referred to anywhere in this code, so itâs very strange for it to get inferred.
đ Expected behavior
Result
is string
since the type extends Brand<string>
.
This is how the example behaves if the âunrelatedFieldâ is removed. Since that field is unrelated to the extends clause, it should have no impact on the inferred type. This is further evidence that âstringâ is the right result, but this case is messing that up somehow.
Additional information about the issue
Another workaround for this is to explicitly reference the type parameter in dummy fields. This is the pattern taken in my TypeCheck library to enable it to control variance before TypeScript 4.7. I found this issue when updating usages of that library to use the built-in variance annotation feature, and it broke some usages of infer.
About this issue
- Original URL
- State: closed
- Created 9 months ago
- Comments: 24 (9 by maintainers)
This has less to do with variance annotations than it has to do with our approximations in inference, and so I feel the best label is ânot a defectâ.
See this example:
When you can infer between two identical type references or aliases, our inference algorithm will avoid any sort of structural checks because it is unnecessary; however, introducing an object type with any contents means that inference must consider the holistic structure.
@MartinJohns and @fatcerberus have given the context around there being nothing to infer from in the underlying structure
Draft
Variance Annotations
TypeScript automatically infers the variance of generic types using its normal type comparison algorithm. In extremely rare cases, this inference process can produce results that are incorrect, or might take longer to measure than is ideal. If that happens, you can use a variance annotation to either correct or speed up the process.
You can optionally annotate type parameters of some types with the variance annotations
in
(contravariant),out
(covariant), orin out
(invariant):If you donât know what those terms means, itâs best to simply avoid this feature, as itâs almost never strictly needed.
Variance annotations are an advanced feature designed to specifically address some very rare situations, and have some important limitations to keep in mind.
Because the purpose of variance annotations is to correct situations where TypeScript produced the wrong result, they arenât strictly validated, so itâs critical to only write a variance annotation if youâre confident that youâre matching the correct structural variance. You should always write the same variance that would be structurally measured by TypeScript (modulo limitations). If youâre not sure what the correct variance of a type is, then itâs not a good situation to write a variance annotation.
Do not suppress errors about incorrect variance errors with
ts-ignore
or similar constructs. This will lead to surprising or incorrect results elsewhere. Trying to change the variance of a type parameter using a variance annotation will not succeed, because this is not what they are designed for.Note that variance annotations do not change the actual variance of a type parameter as it occurs in an instantiated type. Trying to change the variance of a particular position (i.e. making a function argument covariant) will not succeed.
Variance annotations are only used during nominal inference between two types, not structural inference. This means that writing an incorrect variance annotation will generate surprising or wrong results in many cases. Again, to produce consistent and correct results, you must write a variance annotation that would match the structurally-measured variance. Variance annotations which donât match the structural variance will produce surprising results in many cases.
See https://github.com/microsoft/TypeScript/issues/53798 and https://github.com/microsoft/TypeScript/pull/48240#issuecomment-1066527593
Since variance annotations only affect relations between two direct instantiations of the same generic (there are issues about this), if you need to enforce variance even at the structural level, youâll need to keep doing it that way.
https://github.com/microsoft/TypeScript/wiki/FAQ#why-doesnt-type-inference-work-on-this-interface-interface-foot--
Your type
Brand
is empty, thereâs no member to inferT
from. You shouldnât have empty types or unused type arguments.Duplicate of #40796.
This kind of language, and the similar implication throughout, is what worries me. The whole reason weâre here is that you 1) started with an unobserved type parameter, 2) got something that appeared to be working by marking it with a VA, and then 3) got surprised when a structural inference didnât capture the behavior of the VAâd type parameter.
We need to be encouraging people to not do step 2, otherwise theyâll come here at step 3 and say that they were just following instructions and that the behavior at step 3 is a âbugâ when itâs really an incorrect use of a VA.
Usage suggestions are good, but we need a spec. What do variance annotations do? Do they do nothing by add compile errors if they disagree with that the compiler infers? Can they only restrict/constrain access types or can they replace the inferred variance entirely, allowing both restricting and relaxations of use of the type? Is their purpose to be an error at the declaration site if the annotation is wrong, or is getting it wrong undefined behavior?
Also, to follow this usage guideline, I have to know âthe variance that would be structurally measured by TypeScript (modulo limitations)â. How do they interact with mapped types? Is that interaction with mapped types a TypeScript working as intended or one of the âlimitationsâ? How do they works with extends clauses and is that a âlimitationâ or not? Without those answers, following that usage guidance isnât possible.
That means I need a hard answer on which of the behaviors around variance are limitations and which are not. I do not think this is clearly documented.
Personally I find the variance rules TypeScript uses to be frequently incorrect (I wrote and use a library to mitigate this since I kept having real issues with it). Thats using soundness as my definition of correct: Its clearly wrong without strict function checks, and even with them its unsound for properties that are functions (when it infers bivariant), its unsound when it doesnât have enough information (Types are used in whats TS doesnât see as members, like by other Types with infer) its unsound when it deletes private fields etc. Since these seem to be by design its not clear what âwould be structurally measured by TypeScript (modulo limitations).â
What I want to use variance annotations for is three things:
To me how it apparently works sounds like its the opposite of #1? Since to use it you have to confidently know the variance TypeScript (modulo limitations) would have gotten and anyone changing types in the codebase has to ensure they update all variance annotations they might have transiently impacted)? Thus it seems like adding the annotations adds additional risk for bad behavior to crop up later when the types are modified if someone who does so doesnât know to update the variance annotation. If thats really whats going on here thats very unlike other similar languages (ex: C#'s use of them), and unlike most other typing in TypeScript (this makes the code maintaince less likely to cause runtime issues that go unnoticed by the compiler).
Since these cases apparently differ in behavior in important ways, is when each of these happens, and any other ways they differ documented anywhere? Also are these difference included in the âlimitationsâ above (and which version is the limited one? Iâm guessing the structural case?)