Skip to content

Elaborate jsx children elementwise #29264

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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 127 additions & 6 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11196,7 +11196,7 @@ namespace ts {
case SyntaxKind.ArrayLiteralExpression:
return elaborateArrayLiteral(node as ArrayLiteralExpression, source, target, relation);
case SyntaxKind.JsxAttributes:
return elaborateJsxAttributes(node as JsxAttributes, source, target, relation);
return elaborateJsxComponents(node as JsxAttributes, source, target, relation);
case SyntaxKind.ArrowFunction:
return elaborateArrowFunction(node as ArrowFunction, source, target, relation);
}
Expand Down Expand Up @@ -11336,8 +11336,113 @@ namespace ts {
}
}

Copy link
Member

Choose a reason for hiding this comment

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

what

Copy link

@Jessidhia Jessidhia Jan 10, 2019

Choose a reason for hiding this comment

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

<Foo>
</Foo>

will have a JsxText of '\n' but it'll get eliminated before it's converted to the JSX factory call. It won't become a call to React.createElement(Foo, {}, '\n'), but rather React.createElement(Foo).


It will be real text if it's placed between two other JSX elements and doesn't look like newline + indent (<><Foo/> <Bar/></> has 3 children), but I think this is only checking the single child case.

function elaborateJsxAttributes(node: JsxAttributes, source: Type, target: Type, relation: Map<RelationComparisonResult>) {
return elaborateElementwise(generateJsxAttributes(node), source, target, relation);
function *generateJsxChildren(node: JsxElement, getInvalidTextDiagnostic: () => DiagnosticMessage): ElaborationIterator {
if (!length(node.children)) return;
let memberOffset = 0;
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
const nameType = getLiteralType(i - memberOffset);
const elem = getElaborationElementForJsxChild(child, nameType, getInvalidTextDiagnostic);
if (elem) {
yield elem;
}
else {
memberOffset++;
}
}
}

function getElaborationElementForJsxChild(child: JsxChild, nameType: LiteralType, getInvalidTextDiagnostic: () => DiagnosticMessage) {
switch (child.kind) {
case SyntaxKind.JsxExpression:
// child is of the type of the expression
return { errorNode: child, innerExpression: child.expression, nameType };
case SyntaxKind.JsxText:
if (child.containsOnlyWhiteSpaces) {
break; // Whitespace only jsx text isn't real jsx text
}
Copy link

@Jessidhia Jessidhia Jan 10, 2019

Choose a reason for hiding this comment

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

This is hardcoding React-specific knowledge (preact makes no distinction between 1 or many children, it's always array), but it's still better than how it is right now.

This does mean that "The fact that a single-element-child isn't assignable to an array target (because it's not inserted into an array) is real strange." is actually a correct implementation of React at runtime.

Copy link
Member Author

Choose a reason for hiding this comment

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

ick, react-specific jsx checker code. This and the (now incorrect) sfc return type check. 🤕

Choose a reason for hiding this comment

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

This is why React tells you to always use React.Children when dealing with children, but nobody listens... 😥

Copy link
Member Author

Choose a reason for hiding this comment

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

React.Children provides utilities for dealing with the this.props.children opaque data structure.

opaque

image

OK.

// child is a string
return { errorNode: child, innerExpression: undefined, nameType, errorMessage: getInvalidTextDiagnostic() };

Choose a reason for hiding this comment

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

Do you think this fixes children not being caught in excess property checks? IIUC if there is no children prop these both should become neverType, though the error message will probably be confusing.

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 react .d.ts unconditionally mixes children into class components, and so does React.FunctionComponent for those; so if you actually base your component type on the react builtin types, nope. However if you write your own unannotated function component, eg

const Tag = (x: {}) => <div></div>;

// OK
const k1 = <Tag />;
const k2 = <Tag></Tag>;

// Not OK (excess children)
const k3 = <Tag children={<div></div>} />;
const k4 = <Tag><div></div></Tag>;
const k5 = <Tag><div></div><div></div></Tag>;

it seems to work OK.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's not dependent on this PR, however - that's how it currently behaves. (This PR only adjusts error reporting mostly, after all)

Copy link
Member Author

Choose a reason for hiding this comment

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

Ick, works less OK when you have props in common:

const k4 = <Tag key="1"><div></div></Tag>; // should error but doesn't
const k5 = <Tag key="1"><div></div><div></div></Tag>; // should error but doesn't

Copy link
Member Author

Choose a reason for hiding this comment

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

Hahahahahahaha the children prop thing is defs a bug on our part. It's excluded from excess property checks because it's missing a valueDeclaration (whose parent is the containing attributes).

Copy link

@Jessidhia Jessidhia Jan 11, 2019

Choose a reason for hiding this comment

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

Yep, this is that bug I found that time I did try to stop the unconditional mixing in of children :)

Copy link
Member Author

Choose a reason for hiding this comment

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

case SyntaxKind.JsxElement:
case SyntaxKind.JsxSelfClosingElement:
case SyntaxKind.JsxFragment:
// child is of type JSX.Element
return { errorNode: child, innerExpression: child, nameType };
default:
return Debug.assertNever(child, "Found invalid jsx child");
}
}

function elaborateJsxComponents(node: JsxAttributes, source: Type, target: Type, relation: Map<RelationComparisonResult>) {
let result = elaborateElementwise(generateJsxAttributes(node), source, target, relation);
let invalidTextDiagnostic: DiagnosticMessage | undefined;
if (isJsxOpeningElement(node.parent) && isJsxElement(node.parent.parent)) {
const containingElement = node.parent.parent;
const childPropName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node));
const childrenPropName = childPropName === undefined ? "children" : unescapeLeadingUnderscores(childPropName);
const childrenNameType = getLiteralType(childrenPropName);
const childrenTargetType = getIndexedAccessType(target, childrenNameType);
const validChildren = filter(containingElement.children, i => !isJsxText(i) || !i.containsOnlyWhiteSpaces);
if (!length(validChildren)) {
return result;
}
const moreThanOneRealChildren = length(validChildren) > 1;
const arrayLikeTargetParts = filterType(childrenTargetType, isArrayOrTupleLikeType);
const nonArrayLikeTargetParts = filterType(childrenTargetType, t => !isArrayOrTupleLikeType(t));
if (moreThanOneRealChildren) {
if (arrayLikeTargetParts !== neverType) {
const realSource = createTupleType(checkJsxChildren(containingElement, CheckMode.Normal));
result = elaborateElementwise(generateJsxChildren(containingElement, getInvalidTextualChildDiagnostic), realSource, arrayLikeTargetParts, relation) || result;
}
else if (!isTypeRelatedTo(getIndexedAccessType(source, childrenNameType), childrenTargetType, relation)) {
// arity mismatch
result = true;
error(
containingElement.openingElement.tagName,
Diagnostics.This_JSX_tag_s_0_prop_expects_a_single_child_of_type_1_but_multiple_children_were_provided,
childrenPropName,
typeToString(childrenTargetType)
);
}
}
else {
if (nonArrayLikeTargetParts !== neverType) {
const child = validChildren[0];
const elem = getElaborationElementForJsxChild(child, childrenNameType, getInvalidTextualChildDiagnostic);
if (elem) {
result = elaborateElementwise(
(function*() { yield elem; })(),
source,
target,
relation
) || result;
}
}
else if (!isTypeRelatedTo(getIndexedAccessType(source, childrenNameType), childrenTargetType, relation)) {
// arity mismatch
result = true;
error(
containingElement.openingElement.tagName,
Diagnostics.This_JSX_tag_s_0_prop_expects_type_1_which_requires_multiple_children_but_only_a_single_child_was_provided,
childrenPropName,
typeToString(childrenTargetType)
);
}
}
}
return result;

function getInvalidTextualChildDiagnostic() {
if (!invalidTextDiagnostic) {
const tagNameText = getTextOfNode(node.parent.tagName);
const childPropName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node));
const childrenPropName = childPropName === undefined ? "children" : unescapeLeadingUnderscores(childPropName);
const childrenTargetType = getIndexedAccessType(target, getLiteralType(childrenPropName));
const diagnostic = Diagnostics._0_components_don_t_accept_text_as_child_elements_Text_in_JSX_has_the_type_string_but_the_expected_type_of_1_is_2;
invalidTextDiagnostic = { ...diagnostic, key: "!!ALREADY FORMATTED!!", message: formatMessage(/*_dummy*/ undefined, diagnostic, tagNameText, childrenPropName, typeToString(childrenTargetType)) };
}
return invalidTextDiagnostic;
}
}

function *generateLimitedTupleElements(node: ArrayLiteralExpression, target: Type): ElaborationIterator {
Expand Down Expand Up @@ -13478,6 +13583,10 @@ namespace ts {
return isTupleType(type) || !!getPropertyOfType(type, "0" as __String);
}

function isArrayOrTupleLikeType(type: Type): boolean {
return isArrayLikeType(type) || isTupleLikeType(type);
}

function getTupleElementType(type: Type, index: number) {
const propType = getTypeOfPropertyOfType(type, "" + index as __String);
if (propType) {
Expand Down Expand Up @@ -17493,19 +17602,31 @@ namespace ts {
return node === conditional.whenTrue || node === conditional.whenFalse ? getContextualType(conditional) : undefined;
}

function getContextualTypeForChildJsxExpression(node: JsxElement) {
function getContextualTypeForChildJsxExpression(node: JsxElement, child: JsxChild) {
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(getJsxNamespaceAt(node));
return attributesType && !isTypeAny(attributesType) && jsxChildrenPropertyName && jsxChildrenPropertyName !== "" ? getTypeOfPropertyOfContextualType(attributesType, jsxChildrenPropertyName) : undefined;
if (!(attributesType && !isTypeAny(attributesType) && jsxChildrenPropertyName && jsxChildrenPropertyName !== "")) {
return undefined;
}
const childIndex = node.children.indexOf(child);
const childFieldType = getTypeOfPropertyOfContextualType(attributesType, jsxChildrenPropertyName);
return childFieldType && mapType(childFieldType, t => {
if (isArrayLikeType(t)) {
return getIndexedAccessType(t, getLiteralType(childIndex));
}
else {
return t;
}
}, /*noReductions*/ true);
}

function getContextualTypeForJsxExpression(node: JsxExpression): Type | undefined {
const exprParent = node.parent;
return isJsxAttributeLike(exprParent)
? getContextualType(node)
: isJsxElement(exprParent)
? getContextualTypeForChildJsxExpression(exprParent)
? getContextualTypeForChildJsxExpression(exprParent, node)
: undefined;
}

Expand Down
12 changes: 12 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2541,6 +2541,18 @@
"category": "Error",
"code": 2744
},
"This JSX tag's '{0}' prop expects type '{1}' which requires multiple children, but only a single child was provided.": {
"category": "Error",
"code": 2745
},
"This JSX tag's '{0}' prop expects a single child of type '{1}', but multiple children were provided.": {
"category": "Error",
"code": 2746
},
"'{0}' components don't accept text as child elements. Text in JSX has the type 'string', but the expected type of '{1}' is '{2}'.": {
"category": "Error",
"code": 2747
},

"Import declaration '{0}' is using private name '{1}'.": {
"category": "Error",
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -888,7 +888,7 @@ namespace ts {
}

const isMissing = nodeIsMissing(errorNode);
const pos = isMissing
const pos = isMissing || isJsxText(node)
? errorNode.pos
: skipTrivia(sourceFile.text, errorNode.pos);

Expand Down Expand Up @@ -7024,6 +7024,7 @@ namespace ts {
};
}

export function formatMessage(_dummy: any, message: DiagnosticMessage, ...args: (string | number | undefined)[]): string;
export function formatMessage(_dummy: any, message: DiagnosticMessage): string {
let text = getLocaleSpecificMessage(message);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
tests/cases/conformance/jsx/file.tsx(42,11): error TS2322: Type '{ children: Element[]; a: number; b: string; }' is not assignable to type 'SingleChildProp'.
Types of property 'children' are incompatible.
Type 'Element[]' is missing the following properties from type 'Element': type, props
tests/cases/conformance/jsx/file.tsx(42,11): error TS2746: This JSX tag's 'children' prop expects a single child of type 'Element', but multiple children were provided.


==== tests/cases/conformance/jsx/file.tsx (1 errors) ====
Expand Down Expand Up @@ -47,6 +45,4 @@ tests/cases/conformance/jsx/file.tsx(42,11): error TS2322: Type '{ children: Ele
// Error
let k5 = <SingleChildComp a={10} b="hi"><></><Button /><AnotherButton /></SingleChildComp>;
~~~~~~~~~~~~~~~
!!! error TS2322: Type '{ children: Element[]; a: number; b: string; }' is not assignable to type 'SingleChildProp'.
!!! error TS2322: Types of property 'children' are incompatible.
!!! error TS2322: Type 'Element[]' is missing the following properties from type 'Element': type, props
!!! error TS2746: This JSX tag's 'children' prop expects a single child of type 'Element', but multiple children were provided.
40 changes: 8 additions & 32 deletions tests/baselines/reference/checkJsxChildrenProperty2.errors.txt
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
tests/cases/conformance/jsx/file.tsx(14,10): error TS2741: Property 'children' is missing in type '{ a: number; b: string; }' but required in type 'Prop'.
tests/cases/conformance/jsx/file.tsx(17,11): error TS2710: 'children' are specified twice. The attribute named 'children' will be overwritten.
tests/cases/conformance/jsx/file.tsx(31,6): error TS2322: Type '{ children: (Element | ((name: string) => Element))[]; a: number; b: string; }' is not assignable to type 'Prop'.
Types of property 'children' are incompatible.
Type '(Element | ((name: string) => Element))[]' is not assignable to type 'string | Element'.
Type '(Element | ((name: string) => Element))[]' is not assignable to type 'string'.
tests/cases/conformance/jsx/file.tsx(37,6): error TS2322: Type '{ children: (number | Element)[]; a: number; b: string; }' is not assignable to type 'Prop'.
Types of property 'children' are incompatible.
Type '(number | Element)[]' is not assignable to type 'string | Element'.
Type '(number | Element)[]' is not assignable to type 'string'.
tests/cases/conformance/jsx/file.tsx(43,6): error TS2322: Type '{ children: (string | Element)[]; a: number; b: string; }' is not assignable to type 'Prop'.
Types of property 'children' are incompatible.
Type '(string | Element)[]' is not assignable to type 'string | Element'.
Type '(string | Element)[]' is not assignable to type 'string'.
tests/cases/conformance/jsx/file.tsx(49,6): error TS2322: Type '{ children: Element[]; a: number; b: string; }' is not assignable to type 'Prop'.
Types of property 'children' are incompatible.
Type 'Element[]' is not assignable to type 'string | Element'.
Type 'Element[]' is not assignable to type 'string'.
tests/cases/conformance/jsx/file.tsx(31,6): error TS2746: This JSX tag's 'children' prop expects a single child of type 'string | Element', but multiple children were provided.
tests/cases/conformance/jsx/file.tsx(37,6): error TS2746: This JSX tag's 'children' prop expects a single child of type 'string | Element', but multiple children were provided.
tests/cases/conformance/jsx/file.tsx(43,6): error TS2746: This JSX tag's 'children' prop expects a single child of type 'string | Element', but multiple children were provided.
tests/cases/conformance/jsx/file.tsx(49,6): error TS2746: This JSX tag's 'children' prop expects a single child of type 'string | Element', but multiple children were provided.


==== tests/cases/conformance/jsx/file.tsx (6 errors) ====
Expand Down Expand Up @@ -56,43 +44,31 @@ tests/cases/conformance/jsx/file.tsx(49,6): error TS2322: Type '{ children: Elem
let k2 =
<Comp a={10} b="hi">
~~~~
!!! error TS2322: Type '{ children: (Element | ((name: string) => Element))[]; a: number; b: string; }' is not assignable to type 'Prop'.
!!! error TS2322: Types of property 'children' are incompatible.
!!! error TS2322: Type '(Element | ((name: string) => Element))[]' is not assignable to type 'string | Element'.
!!! error TS2322: Type '(Element | ((name: string) => Element))[]' is not assignable to type 'string'.
!!! error TS2746: This JSX tag's 'children' prop expects a single child of type 'string | Element', but multiple children were provided.
<div> My Div </div>
{(name: string) => <div> My name {name} </div>}
</Comp>;

let k3 =
<Comp a={10} b="hi">
~~~~
!!! error TS2322: Type '{ children: (number | Element)[]; a: number; b: string; }' is not assignable to type 'Prop'.
!!! error TS2322: Types of property 'children' are incompatible.
!!! error TS2322: Type '(number | Element)[]' is not assignable to type 'string | Element'.
!!! error TS2322: Type '(number | Element)[]' is not assignable to type 'string'.
!!! error TS2746: This JSX tag's 'children' prop expects a single child of type 'string | Element', but multiple children were provided.
<div> My Div </div>
{1000000}
</Comp>;

let k4 =
<Comp a={10} b="hi" >
~~~~
!!! error TS2322: Type '{ children: (string | Element)[]; a: number; b: string; }' is not assignable to type 'Prop'.
!!! error TS2322: Types of property 'children' are incompatible.
!!! error TS2322: Type '(string | Element)[]' is not assignable to type 'string | Element'.
!!! error TS2322: Type '(string | Element)[]' is not assignable to type 'string'.
!!! error TS2746: This JSX tag's 'children' prop expects a single child of type 'string | Element', but multiple children were provided.
<div> My Div </div>
hi hi hi!
</Comp>;

let k5 =
<Comp a={10} b="hi" >
~~~~
!!! error TS2322: Type '{ children: Element[]; a: number; b: string; }' is not assignable to type 'Prop'.
!!! error TS2322: Types of property 'children' are incompatible.
!!! error TS2322: Type 'Element[]' is not assignable to type 'string | Element'.
!!! error TS2322: Type 'Element[]' is not assignable to type 'string'.
!!! error TS2746: This JSX tag's 'children' prop expects a single child of type 'string | Element', but multiple children were provided.
<div> My Div </div>
<div> My Div </div>
</Comp>;
Loading