Skip to content

Commit 4968989

Browse files
authored
Elaborate jsx children elementwise (microsoft#29264)
* Heavy WIP, but has good contextual typing fix * Add arity error, refine messages and spans * Small error message change * Better error messages, text-specific message
1 parent 35f64fa commit 4968989

15 files changed

+883
-141
lines changed

src/compiler/checker.ts

Lines changed: 127 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11196,7 +11196,7 @@ namespace ts {
1119611196
case SyntaxKind.ArrayLiteralExpression:
1119711197
return elaborateArrayLiteral(node as ArrayLiteralExpression, source, target, relation);
1119811198
case SyntaxKind.JsxAttributes:
11199-
return elaborateJsxAttributes(node as JsxAttributes, source, target, relation);
11199+
return elaborateJsxComponents(node as JsxAttributes, source, target, relation);
1120011200
case SyntaxKind.ArrowFunction:
1120111201
return elaborateArrowFunction(node as ArrowFunction, source, target, relation);
1120211202
}
@@ -11336,8 +11336,113 @@ namespace ts {
1133611336
}
1133711337
}
1133811338

11339-
function elaborateJsxAttributes(node: JsxAttributes, source: Type, target: Type, relation: Map<RelationComparisonResult>) {
11340-
return elaborateElementwise(generateJsxAttributes(node), source, target, relation);
11339+
function *generateJsxChildren(node: JsxElement, getInvalidTextDiagnostic: () => DiagnosticMessage): ElaborationIterator {
11340+
if (!length(node.children)) return;
11341+
let memberOffset = 0;
11342+
for (let i = 0; i < node.children.length; i++) {
11343+
const child = node.children[i];
11344+
const nameType = getLiteralType(i - memberOffset);
11345+
const elem = getElaborationElementForJsxChild(child, nameType, getInvalidTextDiagnostic);
11346+
if (elem) {
11347+
yield elem;
11348+
}
11349+
else {
11350+
memberOffset++;
11351+
}
11352+
}
11353+
}
11354+
11355+
function getElaborationElementForJsxChild(child: JsxChild, nameType: LiteralType, getInvalidTextDiagnostic: () => DiagnosticMessage) {
11356+
switch (child.kind) {
11357+
case SyntaxKind.JsxExpression:
11358+
// child is of the type of the expression
11359+
return { errorNode: child, innerExpression: child.expression, nameType };
11360+
case SyntaxKind.JsxText:
11361+
if (child.containsOnlyWhiteSpaces) {
11362+
break; // Whitespace only jsx text isn't real jsx text
11363+
}
11364+
// child is a string
11365+
return { errorNode: child, innerExpression: undefined, nameType, errorMessage: getInvalidTextDiagnostic() };
11366+
case SyntaxKind.JsxElement:
11367+
case SyntaxKind.JsxSelfClosingElement:
11368+
case SyntaxKind.JsxFragment:
11369+
// child is of type JSX.Element
11370+
return { errorNode: child, innerExpression: child, nameType };
11371+
default:
11372+
return Debug.assertNever(child, "Found invalid jsx child");
11373+
}
11374+
}
11375+
11376+
function elaborateJsxComponents(node: JsxAttributes, source: Type, target: Type, relation: Map<RelationComparisonResult>) {
11377+
let result = elaborateElementwise(generateJsxAttributes(node), source, target, relation);
11378+
let invalidTextDiagnostic: DiagnosticMessage | undefined;
11379+
if (isJsxOpeningElement(node.parent) && isJsxElement(node.parent.parent)) {
11380+
const containingElement = node.parent.parent;
11381+
const childPropName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node));
11382+
const childrenPropName = childPropName === undefined ? "children" : unescapeLeadingUnderscores(childPropName);
11383+
const childrenNameType = getLiteralType(childrenPropName);
11384+
const childrenTargetType = getIndexedAccessType(target, childrenNameType);
11385+
const validChildren = filter(containingElement.children, i => !isJsxText(i) || !i.containsOnlyWhiteSpaces);
11386+
if (!length(validChildren)) {
11387+
return result;
11388+
}
11389+
const moreThanOneRealChildren = length(validChildren) > 1;
11390+
const arrayLikeTargetParts = filterType(childrenTargetType, isArrayOrTupleLikeType);
11391+
const nonArrayLikeTargetParts = filterType(childrenTargetType, t => !isArrayOrTupleLikeType(t));
11392+
if (moreThanOneRealChildren) {
11393+
if (arrayLikeTargetParts !== neverType) {
11394+
const realSource = createTupleType(checkJsxChildren(containingElement, CheckMode.Normal));
11395+
result = elaborateElementwise(generateJsxChildren(containingElement, getInvalidTextualChildDiagnostic), realSource, arrayLikeTargetParts, relation) || result;
11396+
}
11397+
else if (!isTypeRelatedTo(getIndexedAccessType(source, childrenNameType), childrenTargetType, relation)) {
11398+
// arity mismatch
11399+
result = true;
11400+
error(
11401+
containingElement.openingElement.tagName,
11402+
Diagnostics.This_JSX_tag_s_0_prop_expects_a_single_child_of_type_1_but_multiple_children_were_provided,
11403+
childrenPropName,
11404+
typeToString(childrenTargetType)
11405+
);
11406+
}
11407+
}
11408+
else {
11409+
if (nonArrayLikeTargetParts !== neverType) {
11410+
const child = validChildren[0];
11411+
const elem = getElaborationElementForJsxChild(child, childrenNameType, getInvalidTextualChildDiagnostic);
11412+
if (elem) {
11413+
result = elaborateElementwise(
11414+
(function*() { yield elem; })(),
11415+
source,
11416+
target,
11417+
relation
11418+
) || result;
11419+
}
11420+
}
11421+
else if (!isTypeRelatedTo(getIndexedAccessType(source, childrenNameType), childrenTargetType, relation)) {
11422+
// arity mismatch
11423+
result = true;
11424+
error(
11425+
containingElement.openingElement.tagName,
11426+
Diagnostics.This_JSX_tag_s_0_prop_expects_type_1_which_requires_multiple_children_but_only_a_single_child_was_provided,
11427+
childrenPropName,
11428+
typeToString(childrenTargetType)
11429+
);
11430+
}
11431+
}
11432+
}
11433+
return result;
11434+
11435+
function getInvalidTextualChildDiagnostic() {
11436+
if (!invalidTextDiagnostic) {
11437+
const tagNameText = getTextOfNode(node.parent.tagName);
11438+
const childPropName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node));
11439+
const childrenPropName = childPropName === undefined ? "children" : unescapeLeadingUnderscores(childPropName);
11440+
const childrenTargetType = getIndexedAccessType(target, getLiteralType(childrenPropName));
11441+
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;
11442+
invalidTextDiagnostic = { ...diagnostic, key: "!!ALREADY FORMATTED!!", message: formatMessage(/*_dummy*/ undefined, diagnostic, tagNameText, childrenPropName, typeToString(childrenTargetType)) };
11443+
}
11444+
return invalidTextDiagnostic;
11445+
}
1134111446
}
1134211447

1134311448
function *generateLimitedTupleElements(node: ArrayLiteralExpression, target: Type): ElaborationIterator {
@@ -13477,6 +13582,10 @@ namespace ts {
1347713582
return isTupleType(type) || !!getPropertyOfType(type, "0" as __String);
1347813583
}
1347913584

13585+
function isArrayOrTupleLikeType(type: Type): boolean {
13586+
return isArrayLikeType(type) || isTupleLikeType(type);
13587+
}
13588+
1348013589
function getTupleElementType(type: Type, index: number) {
1348113590
const propType = getTypeOfPropertyOfType(type, "" + index as __String);
1348213591
if (propType) {
@@ -17492,19 +17601,31 @@ namespace ts {
1749217601
return node === conditional.whenTrue || node === conditional.whenFalse ? getContextualType(conditional) : undefined;
1749317602
}
1749417603

17495-
function getContextualTypeForChildJsxExpression(node: JsxElement) {
17604+
function getContextualTypeForChildJsxExpression(node: JsxElement, child: JsxChild) {
1749617605
const attributesType = getApparentTypeOfContextualType(node.openingElement.tagName);
1749717606
// JSX expression is in children of JSX Element, we will look for an "children" atttribute (we get the name from JSX.ElementAttributesProperty)
1749817607
const jsxChildrenPropertyName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node));
17499-
return attributesType && !isTypeAny(attributesType) && jsxChildrenPropertyName && jsxChildrenPropertyName !== "" ? getTypeOfPropertyOfContextualType(attributesType, jsxChildrenPropertyName) : undefined;
17608+
if (!(attributesType && !isTypeAny(attributesType) && jsxChildrenPropertyName && jsxChildrenPropertyName !== "")) {
17609+
return undefined;
17610+
}
17611+
const childIndex = node.children.indexOf(child);
17612+
const childFieldType = getTypeOfPropertyOfContextualType(attributesType, jsxChildrenPropertyName);
17613+
return childFieldType && mapType(childFieldType, t => {
17614+
if (isArrayLikeType(t)) {
17615+
return getIndexedAccessType(t, getLiteralType(childIndex));
17616+
}
17617+
else {
17618+
return t;
17619+
}
17620+
}, /*noReductions*/ true);
1750017621
}
1750117622

1750217623
function getContextualTypeForJsxExpression(node: JsxExpression): Type | undefined {
1750317624
const exprParent = node.parent;
1750417625
return isJsxAttributeLike(exprParent)
1750517626
? getContextualType(node)
1750617627
: isJsxElement(exprParent)
17507-
? getContextualTypeForChildJsxExpression(exprParent)
17628+
? getContextualTypeForChildJsxExpression(exprParent, node)
1750817629
: undefined;
1750917630
}
1751017631

src/compiler/diagnosticMessages.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2541,6 +2541,18 @@
25412541
"category": "Error",
25422542
"code": 2744
25432543
},
2544+
"This JSX tag's '{0}' prop expects type '{1}' which requires multiple children, but only a single child was provided.": {
2545+
"category": "Error",
2546+
"code": 2745
2547+
},
2548+
"This JSX tag's '{0}' prop expects a single child of type '{1}', but multiple children were provided.": {
2549+
"category": "Error",
2550+
"code": 2746
2551+
},
2552+
"'{0}' components don't accept text as child elements. Text in JSX has the type 'string', but the expected type of '{1}' is '{2}'.": {
2553+
"category": "Error",
2554+
"code": 2747
2555+
},
25442556

25452557
"Import declaration '{0}' is using private name '{1}'.": {
25462558
"category": "Error",

src/compiler/utilities.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -888,7 +888,7 @@ namespace ts {
888888
}
889889

890890
const isMissing = nodeIsMissing(errorNode);
891-
const pos = isMissing
891+
const pos = isMissing || isJsxText(node)
892892
? errorNode.pos
893893
: skipTrivia(sourceFile.text, errorNode.pos);
894894

@@ -7024,6 +7024,7 @@ namespace ts {
70247024
};
70257025
}
70267026

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

tests/baselines/reference/checkJsxChildrenProperty14.errors.txt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
tests/cases/conformance/jsx/file.tsx(42,11): error TS2322: Type '{ children: Element[]; a: number; b: string; }' is not assignable to type 'SingleChildProp'.
2-
Types of property 'children' are incompatible.
3-
Type 'Element[]' is missing the following properties from type 'Element': type, props
1+
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.
42

53

64
==== tests/cases/conformance/jsx/file.tsx (1 errors) ====
@@ -47,6 +45,4 @@ tests/cases/conformance/jsx/file.tsx(42,11): error TS2322: Type '{ children: Ele
4745
// Error
4846
let k5 = <SingleChildComp a={10} b="hi"><></><Button /><AnotherButton /></SingleChildComp>;
4947
~~~~~~~~~~~~~~~
50-
!!! error TS2322: Type '{ children: Element[]; a: number; b: string; }' is not assignable to type 'SingleChildProp'.
51-
!!! error TS2322: Types of property 'children' are incompatible.
52-
!!! error TS2322: Type 'Element[]' is missing the following properties from type 'Element': type, props
48+
!!! error TS2746: This JSX tag's 'children' prop expects a single child of type 'Element', but multiple children were provided.

tests/baselines/reference/checkJsxChildrenProperty2.errors.txt

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,9 @@
11
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'.
22
tests/cases/conformance/jsx/file.tsx(17,11): error TS2710: 'children' are specified twice. The attribute named 'children' will be overwritten.
3-
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'.
4-
Types of property 'children' are incompatible.
5-
Type '(Element | ((name: string) => Element))[]' is not assignable to type 'string | Element'.
6-
Type '(Element | ((name: string) => Element))[]' is not assignable to type 'string'.
7-
tests/cases/conformance/jsx/file.tsx(37,6): error TS2322: Type '{ children: (number | Element)[]; a: number; b: string; }' is not assignable to type 'Prop'.
8-
Types of property 'children' are incompatible.
9-
Type '(number | Element)[]' is not assignable to type 'string | Element'.
10-
Type '(number | Element)[]' is not assignable to type 'string'.
11-
tests/cases/conformance/jsx/file.tsx(43,6): error TS2322: Type '{ children: (string | Element)[]; a: number; b: string; }' is not assignable to type 'Prop'.
12-
Types of property 'children' are incompatible.
13-
Type '(string | Element)[]' is not assignable to type 'string | Element'.
14-
Type '(string | Element)[]' is not assignable to type 'string'.
15-
tests/cases/conformance/jsx/file.tsx(49,6): error TS2322: Type '{ children: Element[]; a: number; b: string; }' is not assignable to type 'Prop'.
16-
Types of property 'children' are incompatible.
17-
Type 'Element[]' is not assignable to type 'string | Element'.
18-
Type 'Element[]' is not assignable to type 'string'.
3+
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.
4+
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.
5+
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.
6+
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.
197

208

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

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

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

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

0 commit comments

Comments
 (0)