We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
There was an error while loading. Please reload this page.
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
With this PR we introduce a --strictFunctionTypes mode in which function type parameter positions are checked contravariantly instead of bivariantly. The stricter checking applies to all function types, except those originating in method or construcor declarations. Methods are excluded specifically to ensure generic classes and interfaces (such as Array<T>) continue to mostly relate covariantly. The impact of strictly checking methods would be a much bigger breaking change as a large number of generic types would become invariant (even so, we may continue to explore this stricter mode).
--strictFunctionTypes
Array<T>
The --strictFunctionTypes switch is part of the --strict family of switches, meaning that it defaults to on in --strict mode. This PR is therefore a breaking change only in --strict mode.
--strict
Consider the following example in which Animal is the supertype of Dog and Cat:
Animal
Dog
Cat
declare let f1: (x: Animal) => void; declare let f2: (x: Dog) => void; declare let f3: (x: Cat) => void; f1 = f2; / Error with --strictFunctionTypes f2 = f1; / Ok f2 = f3; / Error
The first assignment is permitted in default type checking mode, but flagged as an error in strict function types mode. Intuitively, the default mode permits the assignment because it is possibly sound, whereas strict function types mode makes it an error because it isn't provably sound. In either mode the third assignment is an error because it is never sound.
Another way to describe the example is that the type (x: T) => void is bivariant (i.e. covariant or contravariant) for T in default type checking mode, but contravariant for T in strict function types mode.
(x: T) => void
T
Another example:
interface Comparer<T> { compare(a: T, b: T): number; } declare let animalComparer: Comparer<Animal>; declare let dogComparer: Comparer<Dog>; animalComparer = dogComparer; / Ok because of bivariance dogComparer = animalComparer; / Ok
In --strictFunctionTypes mode the first assignment is still permitted because compare is declared as a method. Effectively, T is bivariant in Comparer<T> because it is used only in method parameter positions. However, changing compare to be a property with a function type causes stricter checking to take effect:
compare
Comparer<T>
interface Comparer<T> { compare: (a: T, b: T) => number; } declare let animalComparer: Comparer<Animal>; declare let dogComparer: Comparer<Dog>; animalComparer = dogComparer; / Error dogComparer = animalComparer; / Ok
The first assignment is now an error. Effectively, T is contravariant in Comparer<T> because it is used only in function type parameter positions.
By the way, note that whereas some languages (e.g. C# and Scala) require variance annotations (out/in or +/-), variance emerges naturally from the actual use of a type parameter within a generic type due to TypeScript's structural type system.
out
in
+
-
We also improve type inference involving contravariant positions in this PR:
function combine<T>(...funcs: ((x: T) => void)[]): (x: T) => void { return x => { for (const f of funcs) f(x); } } function animalFunc(x: Animal) {} function dogFunc(x: Dog) {} let combined = combine(animalFunc, dogFunc); / (x: Dog) => void
Above, all inferences for T originate in contravariant positions, and we therefore infer the best common subtype for T. This contrasts with inferences from covariant positions, where we infer the best common supertype. Note that inferences from covariant positions have precedence over inferences from contravariant positions.
Fixes #6102. Fixes #7472. Fixes #9514. Fixes #9765. Fixes #12498. Fixes #13248. Fixes #15579. Fixes #18337. Fixes #18466.
Also related are #14973.
Sorry, something went wrong.
Introduce --strictFunctionTypes mode
12f5dd8
Use dedicated marker types for variance determination
f8ff7f7
Add quick path for computing array variance as it is already known
670d711
Handle contravariance in type inference
a0fa69f
Add comments
b58e0fb
Handle special case of 'void' type arguments for covariant type param…
84f7afd
…eters
Accept new baselines
54eadef
Use methods in dom.generated.d.ts to opt out of strict checks
44cc8c5
Update tsconfig baselines
dd466ae
Revert dom.generated.d.ts and fix duplicate declarations
24698dd
Properly flag and structurally compare marker type references
f8e2cc1
Update comment
589e1f4
I ran RWC on this and saw changes only in elaborations and order of printing union types. No errors were added or removed, and no types changed.
The changes appear to result from better type caching leading to less elaboration. The only interesting result was from the Azure Framework tests, where one incorrectly duplicated error disappeared entirely, maybe because the whole error was already issued just prior.
I'll run DefinitelyTyped next and look for diffs.
@sandersn When running DefinitelyTyped, be sure to run both with and without --strict. I'm particularly interested in the results with --strict.
Ah, good point. I forgot --strict the first time around. First I compiled everything using 0ac8406, the commit before this PR. Then I compiled again with this PR's commits and diffed the changes. Interestingly, a few packages on DefinitelyTyped still have more errors even without --strictFunctionTypes. The errors look like they come from tighter variance checking too, so it's likely that some changes are escaping from behind the flag.
Take a look at the test files for recompose, rx-lite and rx.wamp (which depends on rx-lite). Recompose is clearer to see (the rx* errors are obscured by overloads in rx-lite):
recompose-tests.tsx(248,7): error TS2345: Argument of type 'InferableComponentEnhancerWithProps<{ count: number; } & { update: (state: number) => number; }, {}>' is not assignable to parameter of type 'ComponentEnhancer<any, any> | InferableComponentEnhancerWithProps<{}, {}>'. Type 'InferableComponentEnhancerWithProps<{ count: number; } & { update: (state: number) => number; }, {}>' is not assignable to type 'InferableComponentEnhancerWithProps<{}, {}>'. Type '{}' is not assignable to type '{ count: number; } & { update: (state: number) => number; }'. Type '{}' is not assignable to type '{ count: number; }'. Property 'count' is missing in type '{}'.
Notice that the direction of assignability switches, I think because of contravariance.
In total, I noticed changes with
I'll take a look at the results with --strictFunctionChecks turned on next. It's only halfway done, but there are already lots of failures, so I'll only provide a summary.
--strictFunctionChecks
Always perform structural comparison when variance check fails
afc8a26
Add tests
70e8f73
@sandersn With the latest commits we revert to always performing a structural comparison when the relationships indicated by getVariances don't hold. In effect, the variance information computed by getVariances is now an indication of which relationships to try first as an optimization, but it doesn't preclude other relationships. In the default checking mode we now behave exactly the same way as before, so I would expect no changes to RWC baselines or DefinitelyTyped. I would expect to see new errors only in --strictFunctionTypes mode.
getVariances
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
When does this case occur?
This happens when we have a synthetic type parameter, such as the type parameters we create for tuple types and the markerXXXType type parameters we use for variance determination.
markerXXXType
LGTM but I'd like to see what happens if we run RWC with this forced on
With the latest commits, both RWC and DT crash much more due to running out of memory. I only get about 2/3 of the RWC tests to succeed, and DT crashes at 'bookshelf'.
@sandersn Is that with or without --strictFunctionTypes?
With --strictFunctionTypes
@sandersn It is likely because we do more structural comparisons of types. There are enough edge cases (e.g. any or void type arguments) where types don't act exactly according to the gathered variance information, so if the optimized first check of the type arguments doesn't succeed we need to do a structural comparison. We'll have to look at the specific cases, but it's not clear what else we could do.
any
void
Add error elaboration test
c2344e0
Latest commits address code review feedback. Also, we improve error elaboration for generic types where one or more type parameters are invariant. In some cases generic types that are covariant in regular type checking mode become invariant in --strictFunctionTypes mode because one or more type parameters are used in both co- and contravariant positions with the generic type. In order to make it easier to diagnose why such types are invariant, if any of the type parameters are invariant we reset the reported errors and instead force a structural comparison (which will include elaborations that reveal the reason).
884c72e
@ahejlsberg do you have feedback on my latest example?
@ahejlsberg Even though you mention only 3% of type packages are affected, I think the real negative impact is in much higher considering that major packages like angular, express, hapi, cucumber, selenium-webdriver etc. are affected. Hopefully the undocummented "skipLibCheck" option can rescue some projects.
@xnnkmd The list above doesn't show it, but in practically all cases (including all of the ones you mention) the errors are in the tests associated with the type package, not in the package itself. So, there should be no need to use --skipLibCheck.
--skipLibCheck
the errors are in the tests associated with the type package, not in the package itself
@ahejlsberg Can you post a list of packages where only the package itself is affected? (not the tests)
gentlemen, pardon me, but i don't understand your attitude towards these mighty new features... even if we break 97% of all code out there, even then:
it's under the flag --strictFunctionTypes, so if you are scared of it, stay away from it! so if you did stay away from it, you are in the old safe TypeScript and nothing brakes
secondly, the code that breaks is ill and it's a good thing that you see it breaks, it means you know where you weak parts are, so please be grateful, will you?
DefinitelyTyped/DefinitelyTyped#20219 fixes the packages on DefinitelyTyped that break with --strictFunctionTypes. Plus it updates the tests for many "popular" packages whose tests will break so you can take a look to see how you might have to update your code.
Packages fixed by DefinitelyTyped/DefinitelyTyped#20219:
I also fixed tests in
Notes:
sandersn sandersn approved these changes
RyanCavanaugh RyanCavanaugh left review comments
mhegazy Awaiting requested review from mhegazy
DanielRosenwasser Awaiting requested review from DanielRosenwasser
Successfully merging this pull request may close these issues.