Skip to content

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

Merged
merged 8 commits into from
Feb 6, 2018

Conversation

weswigham
Copy link
Member

Fixes the issue I talked about in #21381, which is this:

In JSX, this pattern is presently broken because our JSX type calculation code aggressively gets the apparent type of types it uses to construct the attributes type - contrary to what we'd like for appropriate contextual typing (since the apparent type replaces type parameters with their constraints, preventing us from drawing inferences). I'll have a PR up to fix that shortly.

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 like inferTypeArguments 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.

@weswigham
Copy link
Member Author

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).


return mapType(valueType, signturesToParameterTypes);

function signturesToParameterTypes(valueType: Type) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling (signatures)

const paramType = getTypeAtPosition(signature, 0);
inferTypes(context.inferences, sourceAttributesType, paramType);
function inferJsxTypeArguments(signature: Signature, node: JsxOpeningLikeElement, context: InferenceContext): Type[] {
{
Copy link
Member

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 ?

Copy link
Member Author

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.

for (const signature of signatures) {
if (signature.typeParameters) {
const isJavascript = isInJavaScriptFile(node);
const inferenceContext = createInferenceContext(signature, /*flags*/ isJavascript ? InferenceFlags.AnyDefault : 0);
Copy link
Member

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)

if (intrinsicClassAttribs !== unknownType) {
const typeParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(intrinsicClassAttribs.symbol);
if (typeParams) {
if (typeParams.length === 1) {
Copy link
Member

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

// There is no property named 'props' on this instance type
return emptyObjectType;
}
else if (isTypeAny(attributesType) || (attributesType === unknownType)) {
Copy link
Member

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

Copy link
Member

@RyanCavanaugh RyanCavanaugh left a 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 👍

@weswigham
Copy link
Member Author

@RyanCavanaugh Done - I've also consolidated a few pieces of functionality.

Copy link
Member

@sandersn sandersn left a 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);
Copy link
Member

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?

Copy link
Member Author

@weswigham weswigham Feb 3, 2018

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.

Copy link
Member Author

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.

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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Propertyname should capitalize Name

return anyType;
}

return mapType(valueType, signaturesToParameterTypes);
Copy link
Member

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

@@ -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.
Copy link
Member

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);
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Member Author

@weswigham weswigham Feb 2, 2018

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)

if (intrinsicClassAttribs !== unknownType) {
const typeParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(intrinsicClassAttribs.symbol);
apparentAttributesType = intersectTypes(
length(typeParams)
Copy link
Member

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?

Copy link
Member Author

@weswigham weswigham Feb 3, 2018

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. 🌞

Copy link
Member Author

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)) {
Copy link
Member

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?

Copy link
Member Author

@weswigham weswigham Feb 2, 2018

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
Copy link
Member

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.

Copy link
Member Author

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.

@weswigham
Copy link
Member Author

ping @sandersn I'm done implementing your feedback; wanna take another look when you get a chance?

@chrisbenti
Copy link

this fixes my repo @ lyft

@chrisbenti
Copy link

Thanks @weswigham !

@@ -14788,7 +14788,7 @@ namespace ts {
}
}
else {
childrenTypes.push(checkExpression(child, checkMode));
childrenTypes.push(checkExpressionForMutableLocation(child, checkMode));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why mutable location?

Copy link
Member Author

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.

@@ -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);
Copy link
Member

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.

Copy link
Member Author

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. :(

Copy link
Member

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. :]

Copy link
Member

@sandersn sandersn left a 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.

@weswigham
Copy link
Member Author

@sandersn Test added. (And a small change to actually-do-the-right-thing-and-not-throw). Thanks. 🐱

@weswigham
Copy link
Member Author

@sandersn I ran the dt tests and this PR doesn't affect any baselines, when compared to master. 🌞 (I somehow feel like this is a failing of the tests on dt, because it definitely should - leaflet, for example, definitely has generic jsx elements in it - the tests must not use them extensively)

@weswigham weswigham merged commit 17554ff into master Feb 6, 2018
weswigham added a commit to weswigham/TypeScript that referenced this pull request Feb 6, 2018
…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
@weswigham weswigham deleted the fix-jsx-contextual-types branch February 6, 2018 01:03
weswigham added a commit to weswigham/TypeScript that referenced this pull request Feb 7, 2018
…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
weswigham added a commit that referenced this pull request Feb 7, 2018
…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
@microsoft microsoft locked and limited conversation to collaborators Jul 3, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants