-
Notifications
You must be signed in to change notification settings - Fork 12.9k
fix(50858): parameters in function signatures inferred from default generic types sometimes not assigned instantiated types #50960
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
Conversation
@microsoft-github-policy-service agree |
As a bystander - I applaud the description of this PR 👏 |
@typescript-bot perf test this |
Heya @RyanCavanaugh, I've started to run the perf test suite on this PR at 92631fe. You can monitor the build here. Update: The results are in! |
Heya @RyanCavanaugh, I've started to run the diff-based top-repos suite on this PR at 92631fe. You can monitor the build here. Update: The results are in! |
@RyanCavanaugh Here they are:
CompilerComparison Report - main..50960
System
Hosts
Scenarios
TSServerComparison Report - main..50960
System
Hosts
Scenarios
Developer Information: |
@RyanCavanaugh Here are the results of running the top-repos suite comparing Everything looks good! |
Ha, thanks! I hope it helps the reviewers more than it confuses them 😅 |
// If the apparent type is any, we cannot lose any information by instantiating, | ||
// so we might as well try and possibly get a useful type | ||
if (apparentType === anyType) { | ||
return instantiateInstantiableTypes(contextualType, inferenceContext.nonFixingMapper); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In what cases this is helpful? It's not totally clear to me what exactly the apparent type is but if I take into account the comment above the getApparentType
then it should return the base constraint for type parameters.
Given that this PR introduces only tests that have a base constraint that is not any
- I wonder, what existing test cases does this branch satisfy?
if (apparentType === anyType) { | ||
return instantiateInstantiableTypes(contextualType, inferenceContext.nonFixingMapper); | ||
} | ||
else if((apparentType.flags & TypeFlags.Union)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
else if((apparentType.flags & TypeFlags.Union)) { | |
else if ((apparentType.flags & TypeFlags.Union)) { | |
|
||
if (!signatureList) { | ||
// At this point, the signature type is guaranteed to eventually collapse into any | ||
// because it is a union of incompatible function signatures |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// because it is a union of incompatible function signatures | |
// because it is a union of incompatible function signatures or there is no signature at all | |
@mxsdev I think it is a good idea to fix this issue, but I think there's a much simpler way to implement it. I will put up a PR shortly. |
Fixes #50858
This issue is specifically about functions within the arguments of generic-inferring call expressions. Early in the execution flow, the internal function's signature will only be instantiated with types when some inferences have already been made about them, but this misses the case where those generics have default values. Instead of taking default values, the signature falls back to a base type. If that base type is a union of incompatible signatures or
any
, then the signature's parameters will be set toany
.Later in the execution flow, the signature is properly instantiated; however by that time diagnostics have already been created and the signature's symbol type may already have been set to
any
.See below for a more in-depth look at the call stack.
Example
Consider the following test:
The function call
f1
should take a default type parameter of"a"
for type parameterT
, and so we expectevent
to take its contextual type ofstring
. However, due to this issue, it is of typeany
and throws anoImplicitAny
diagnostic.Call Stack
Call Stack Analysis of Example (in checker.ts)
resolveCallExpression
on call expressionf1((event) => { })
->resolveCall
->chooseOverload
chooseOverload
: NotypeParameters
are specified, soinferTypeArguments
is called twice. The first call skips context sensitive parameters (such as(event) => {}
), so we are interested in the second callinferTypeArguments
: callscheckExpressionWithContextualType
to get the type of each argument before making any type inferencescheckExpressionWithContextualType
-> ... ->contextuallyCheckFunctionExpressionOrObjectLiteralMethod
contextuallyCheckFunctionExpressionOrObjectLiteralMethod
: Gets contextual signature of argument by callinggetContextualSignature
getContextualSignature
: Gets type fromgetApparentTypeOfContextualType
getApparentTypeOfContextualType
:contextualType
is the typeType[T]
of kindIndexedAccess
. We then try and instantiate that type by callinginstantiateContextualType
instantiateContextualType
: Since no inferences have been made, the type is not instantiated, and thecontextualType
is returnedinstantiateContextualType
does nothing, so we find the apparent type ofType[T]
, which in this case is the base type, which is(e: string) => void|(e: number) => void
undefined
is returnedundefined
, the parameter type is assigned withassignNonContextualParameterTypes
assignNonContextualParameterTypes
: CallsassignParameterType
withundefined
type on each parameterundefined
, another attempt is made to infer it contextually, which ultimately hitsgetContextualSignature
again which like before returnsundefined
widenTypeForVariableLikeDeclaration
: Type isundefined
soanyType
is returned and a diagnostic is throwninferTypes
is now called, which makes type inferencesgetSignatureInstantiation
, and the resulting signature is correct (has(event: string) => void
as a parameter)getSignatureApplicabilityError
is called, which flows through the same call stack as before with the instantiated signature, untilcontextuallyCheckFunctionExpressionOrObjectLiteralMethod
because the previous execution stack already "checked the context" and setNodeCheckFlags.ContextChecked
.This issue is caused by
instantiateContextualType
, which is in the call stack ofinferTypeArguments
inchooseOverload
. There is a cyclic dependency: the result ofinferTypeArguments
is necessary to fully instantiate the signature withgetSignatureInstantiation
, but (it would seem) a fully instantiated signature is expected by the return result ofinstantiateContextualType
withingetApparentTypeOfContextualType
, which is in the call stack ofinferTypeArguments
.Solution
The solution presented is to instantiate from within
instantiateContextualType
whenever the apparent type ofcontextualType
is "degenerate" - that is, when it is eitherany
or a union of incompatible signatures - since, in this case, essentially no further information on the parameters of the signature can be gleamed if we return it. So, in the worst case, we get back another degenerate type from instantiation, but in the best case, we get more type information.In cases like the previous example, the call stack off of
inferTypeArguments
will generate the correct signature, and the correct parameter type will be assigned.Further thoughts
I doubt this fixes every single possible instance of this bug, but it fixes the linked issue and many others. But I presume the types of the function parameters should be set after or during the instantiation of the signature, not during
inferTypeArguments
. It is indeed rather strange as well that a function named "assignNonContextualParameterTypes
" ends up trying to infer a lot of contextual information. But my familiarity with the codebase is not excellent, so if somebody can clear that up, please do!One more potential concern I had was that
instantiateContextualType
may now end up inferring "too much" - perhaps returning a non-any type where an any type is required. All of the tests pass, however, so this doesn't seem likely.My original assumption was that
inferTypeArguments
should have a flag to never result in a call ofassignParameterType
, however I wasn't able to get this to work without destroying generic inference; it seems that many instances of type inference require types to be set from the call toinferTypeArguments
, and not from the call togetSignatureApplicabilityError
with the instantiated signature.Another thought was to allow
getSignatureApplicabilityError
to set the type by resettingNodeCheckFlags.ContextChecked
on the arguments. This does not work, however, sinceassignParameterType
prevents setting the parameter type to one different than was cached; moreover, the diagnostic errors have already been thrown.Note that removing the check in
instantiateContextualType
that "no inferences have been made" understandably breaks the codebase as well, since this will (I assume) instantiate preemptively in many cases.If anyone has any ideas on how to make the previous approaches work, please respond, since I think they're significantly more elegant solutions to the problem than the one presented.