-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Fix JSX contextual types to not eagerly become apparent, use 2-pass inference for JSX #21383
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
Note that this undoes the fix in #21108, but still fixes the underlying issue, since divorcing the codepaths used by checking vs contextual typing for determining the correct signatures to look at accomplishes the same thing (the contextual typing version never instantiates and doesn't get the apparent type unless it has to, the check version continues to behave as it does today). |
src/compiler/checker.ts
Outdated
|
||
return mapType(valueType, signturesToParameterTypes); | ||
|
||
function signturesToParameterTypes(valueType: Type) { |
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.
Spelling (signatures
)
src/compiler/checker.ts
Outdated
const paramType = getTypeAtPosition(signature, 0); | ||
inferTypes(context.inferences, sourceAttributesType, paramType); | ||
function inferJsxTypeArguments(signature: Signature, node: JsxOpeningLikeElement, context: InferenceContext): Type[] { | ||
{ |
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.
If using a block like this isn't a lint violation already, it should be.
Is it actually unsafe to reuse paramType
?
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.
The blocks are just there for organization (as in inferTypeArguments
each pass is an identifiable logical block thanks to each cycle of argument iteration), not to allow reuse of the same name safely. I'll remove them, if you'd prefer.
src/compiler/checker.ts
Outdated
for (const signature of signatures) { | ||
if (signature.typeParameters) { | ||
const isJavascript = isInJavaScriptFile(node); | ||
const inferenceContext = createInferenceContext(signature, /*flags*/ isJavascript ? InferenceFlags.AnyDefault : 0); |
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.
We should add InferenceFlags.None
so that this ternary can be made typesafe (as-is it'd be possible to pass the wrong kind of enum to createInferenceContext
)
src/compiler/checker.ts
Outdated
if (intrinsicClassAttribs !== unknownType) { | ||
const typeParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(intrinsicClassAttribs.symbol); | ||
if (typeParams) { | ||
if (typeParams.length === 1) { |
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.
nit: &&
with the above condition to reduce nesting
src/compiler/checker.ts
Outdated
// There is no property named 'props' on this instance type | ||
return emptyObjectType; | ||
} | ||
else if (isTypeAny(attributesType) || (attributesType === unknownType)) { |
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.
isTypeAny(unknownType)
is already true
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.
LGTM after minor fixes 👍
@RyanCavanaugh Done - I've also consolidated a few pieces of functionality. |
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.
Mostly questions about how things work.
return undefined; | ||
} | ||
function getContextualTypeForChildJsxExpression(node: JsxElement) { | ||
const attributesType = getApparentTypeOfContextualType(node.openingElement.tagName); |
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.
Is this function tested by the new test? Maybe an existing test?
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.
Added test, it was broken, then I fixed jsx children contextual typing - the issue was that I was checking the jsx attributes, but the jsx children aren't in the subtree of the attributes (despite contributing to its type), so didn't have the contextual type or mapper set by checkExpressionWithContextualType
in their heirarchy.
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.
So now it is tested. 🌞 Thanks.
src/compiler/checker.ts
Outdated
function getContextualTypeForChildJsxExpression(node: JsxElement) { | ||
const attributesType = getApparentTypeOfContextualType(node.openingElement.tagName); | ||
// JSX expression is in children of JSX Element, we will look for an "children" atttribute (we get the name from JSX.ElementAttributesProperty) | ||
const jsxChildrenPropertyName = getJsxElementChildrenPropertyname(); |
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.
Propertyname should capitalize Name
src/compiler/checker.ts
Outdated
return anyType; | ||
} | ||
|
||
return mapType(valueType, signaturesToParameterTypes); |
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.
Personally I would inline signaturesToParameterTypes
src/compiler/checker.ts
Outdated
@@ -15298,16 +15331,16 @@ namespace ts { | |||
// 3. Check if the two are assignable to each other | |||
|
|||
|
|||
// targetAttributesType is a type of an attributes from resolving tagName of an opening-like JSX element. |
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.
Typo:attribute
function inferJsxTypeArguments(signature: Signature, node: JsxOpeningLikeElement, context: InferenceContext): Type[] { | ||
// Skip context sensitive pass | ||
const skipContextParamType = getTypeAtPosition(signature, 0); | ||
const checkAttrTypeSkipContextSensitive = checkExpressionWithContextualType(node.attributes, skipContextParamType, identityMapper); |
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.
I don’t understand this pass, or at least the name of this pass. We grab the type of the first parameter, then use it as the contextual type against node.attributes, instead of just taking the type from outside this function. But then we call getTypeAtPosition again — do we expect something different this time?
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.
Yep, we do expect something different. Namely we expect contextual parameter types to have been assigned from the first pass. We do the same thing in normal inference where we call checkExpression separately in each pass on an argument.
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.
Oh, right, because inferTypes is side-effecting. Or is is checkExpressionWithContextualType.
I still don't understand how this skips the context param though. Or does it skip the context[ual type] of the param?
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.
checkExpressionWithContextualType
is the sideffecting one. It can cause assignments to cached parameter types when called with an inference context as the contextual mapper. (so the first call isn't side-effecting, but is inference generating, while the second is side-effecting and inference generating)
src/compiler/checker.ts
Outdated
if (intrinsicClassAttribs !== unknownType) { | ||
const typeParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(intrinsicClassAttribs.symbol); | ||
apparentAttributesType = intersectTypes( | ||
length(typeParams) |
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 the previous version if typeParams had length 0, then apparentAttributesType doesn’t change. Now it gets intersected with intrinsicClassAttribs. Is that OK?
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.
A length of 0 for type params isn't possible; we don't make a type param list, then, I don't think. HOWEVER, the bigger issue that you've called to attention is that if the intrinsic class attributes type has more than one parameter (not correct, but I don't think there's an error for it), this now intersects a class type reference that's missing arguments (instead of nothing), I think it should do our normal type parameter-filling behavior using fillMissingTypeArguments
. Meaning you should be able to write interface ElementClass<T, Props = {}> { /*...*/ }
and actually have it work. I'll add that now. 🌞
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.
Done. I think this is now much more consistent with how we handle type arguments and generics elsewhere.
// There is no property named 'props' on this instance type | ||
return emptyObjectType; | ||
} | ||
else if (isTypeAny(attributesType)) { |
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.
Why drop the unknownType check? Or does isTypeAny work for unknownType too?
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.
Yes. @RyanCavanaugh pointed that out:
const unknownType = createIntrinsicType(TypeFlags.Any, "unknown");
//...
function isTypeAny(type: Type) {
return type && (type.flags & TypeFlags.Any) !== 0;
}
@@ -3846,6 +3846,7 @@ namespace ts { | |||
} | |||
|
|||
export const enum InferenceFlags { | |||
None = 0, // No special inference behaviors |
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.
I kind of like 0. It’s pretty succinct.
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.
But we have None
for so many other enums - plus, it also clarifies what 0
actually does at the callsite to someone reading the code.
ping @sandersn I'm done implementing your feedback; wanna take another look when you get a chance? |
this fixes my repo @ lyft |
Thanks @weswigham ! |
@@ -14788,7 +14788,7 @@ namespace ts { | |||
} | |||
} | |||
else { | |||
childrenTypes.push(checkExpression(child, checkMode)); | |||
childrenTypes.push(checkExpressionForMutableLocation(child, checkMode)); |
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.
Why mutable location?
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.
jsxchildren are like a jsxattribute are like object members and should be checked the same way.
src/compiler/checker.ts
Outdated
@@ -14095,7 +14104,7 @@ namespace ts { | |||
} | |||
} | |||
|
|||
return getUnionType(map(signatures, ctor ? getJsxPropsTypeFromConstructSignature : getJsxPropsTypeFromCallSignature), UnionReduction.None); | |||
return getUnionType(map(signatures, ctor ? isJs ? getJsxPropsTypeFromConstructSignatureJs : getJsxPropsTypeFromConstructSignature : getJsxPropsTypeFromCallSignature), UnionReduction.None); |
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.
It seems a lot easier to read to write this as x => getJsxPropsTypeFromConstructSignature(x, isJs)
— and everywhere else you added function pairs.
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.
But that'd introduce a lambda on every invocation here, and this is called in contextual typing (which is done often), whereas this way only two functions get made ever. I do agree that that looks better, I just think it'd be detrimental to perf. :(
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.
[1] Did you measure? You’re still doing dynamic dispatch here so I can’t imagine the numbers would be that different (though I am not sure how good the VMs are at lifting lambdas).
[2] At least extract the double-nested conditional to a local. That’s just egregious. :]
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.
Looks good after you [1] add a test for fillMissingTypeArguments [2] make sure that DefinitelyTyped doesn’t break too much from the improved inferences.
@sandersn Test added. (And a small change to actually-do-the-right-thing-and-not-throw). Thanks. 🐱 |
@sandersn I ran the |
…nference for JSX (microsoft#21383) * Fix JSX contextual types to not eagerly become apparent * Apply changes from code review, unify common code * Fix jsx children contextual typing * Light code review feedback * Use fillMissingTypeArguments * Accept nonliteral jsx child type * Add test for the fillMissingTypeArguments case
…nference for JSX (microsoft#21383) * Fix JSX contextual types to not eagerly become apparent * Apply changes from code review, unify common code * Fix jsx children contextual typing * Light code review feedback * Use fillMissingTypeArguments * Accept nonliteral jsx child type * Add test for the fillMissingTypeArguments case
…nference for JSX (#21383) (#21749) * Fix JSX contextual types to not eagerly become apparent * Apply changes from code review, unify common code * Fix jsx children contextual typing * Light code review feedback * Use fillMissingTypeArguments * Accept nonliteral jsx child type * Add test for the fillMissingTypeArguments case
Fixes the issue I talked about in #21381, which is this:
While I was there, I also generally did a cleanup and conceptual unification pass on the JSX inference code (most of it was actually necessary just to ensure contextual typing didn't trigger inference in a loop when calculating the inferred type for a parameter). Now jsx always gets a 2-pass inference (no 3rd pass for return type inference, since right now jsx expressions have a static type of
JSX.Element
), just like a normal function call, and is structured much more likeinferTypeArguments
is in general (including reevaluating the type of the argument expression between each pass). The lack of a second pass could have prevented context sensitive generic functions from acquiring the correct type (which are now represented in the test in this PR).While working on the test case for this, I encountered #21382 (which I thought was an inference issue until I checked the type baselines and compiler output), which as it turns out is not a JSX-specific issue, so I'm opening this PR despite knowing that quickinfo in a file like the test (or like will be in
formik
's user test, once they update their type definitions to be inferrable) will be wrong.