Skip to content

Commit e1d346e

Browse files
authored
Infer tuples for jsx children if contextually typed by a tuple (microsoft#27409)
1 parent deeb401 commit e1d346e

6 files changed

+260
-21
lines changed

src/compiler/checker.ts

+31-21
Original file line numberDiff line numberDiff line change
@@ -17189,31 +17189,14 @@ namespace ts {
1718917189
const minLength = elementCount - (hasRestElement ? 1 : 0);
1719017190
// If array literal is actually a destructuring pattern, mark it as an implied type. We do this such
1719117191
// that we get the same behavior for "var [x, y] = []" and "[x, y] = []".
17192+
let tupleResult: Type | undefined;
1719217193
if (inDestructuringPattern && minLength > 0) {
1719317194
const type = cloneTypeReference(<TypeReference>createTupleType(elementTypes, minLength, hasRestElement));
1719417195
type.pattern = node;
1719517196
return type;
1719617197
}
17197-
if (contextualType && contextualTypeIsTupleLikeType(contextualType)) {
17198-
const pattern = contextualType.pattern;
17199-
// If array literal is contextually typed by a binding pattern or an assignment pattern, pad the resulting
17200-
// tuple type with the corresponding binding or assignment element types to make the lengths equal.
17201-
if (!hasRestElement && pattern && (pattern.kind === SyntaxKind.ArrayBindingPattern || pattern.kind === SyntaxKind.ArrayLiteralExpression)) {
17202-
const patternElements = (<BindingPattern | ArrayLiteralExpression>pattern).elements;
17203-
for (let i = elementCount; i < patternElements.length; i++) {
17204-
const e = patternElements[i];
17205-
if (hasDefaultValue(e)) {
17206-
elementTypes.push((<TypeReference>contextualType).typeArguments![i]);
17207-
}
17208-
else if (i < patternElements.length - 1 || !(e.kind === SyntaxKind.BindingElement && (<BindingElement>e).dotDotDotToken || e.kind === SyntaxKind.SpreadElement)) {
17209-
if (e.kind !== SyntaxKind.OmittedExpression) {
17210-
error(e, Diagnostics.Initializer_provides_no_value_for_this_binding_element_and_the_binding_element_has_no_default_value);
17211-
}
17212-
elementTypes.push(strictNullChecks ? implicitNeverType : undefinedWideningType);
17213-
}
17214-
}
17215-
}
17216-
return createTupleType(elementTypes, minLength, hasRestElement);
17198+
else if (tupleResult = getArrayLiteralTupleTypeIfApplicable(elementTypes, contextualType, hasRestElement, elementCount)) {
17199+
return tupleResult;
1721717200
}
1721817201
else if (forceTuple) {
1721917202
return createTupleType(elementTypes, minLength, hasRestElement);
@@ -17222,6 +17205,31 @@ namespace ts {
1722217205
return getArrayLiteralType(elementTypes, UnionReduction.Subtype);
1722317206
}
1722417207

17208+
function getArrayLiteralTupleTypeIfApplicable(elementTypes: Type[], contextualType: Type | undefined, hasRestElement: boolean, elementCount = elementTypes.length) {
17209+
if (contextualType && contextualTypeIsTupleLikeType(contextualType)) {
17210+
const minLength = elementCount - (hasRestElement ? 1 : 0);
17211+
const pattern = contextualType.pattern;
17212+
// If array literal is contextually typed by a binding pattern or an assignment pattern, pad the resulting
17213+
// tuple type with the corresponding binding or assignment element types to make the lengths equal.
17214+
if (!hasRestElement && pattern && (pattern.kind === SyntaxKind.ArrayBindingPattern || pattern.kind === SyntaxKind.ArrayLiteralExpression)) {
17215+
const patternElements = (<BindingPattern | ArrayLiteralExpression>pattern).elements;
17216+
for (let i = elementCount; i < patternElements.length; i++) {
17217+
const e = patternElements[i];
17218+
if (hasDefaultValue(e)) {
17219+
elementTypes.push((<TypeReference>contextualType).typeArguments![i]);
17220+
}
17221+
else if (i < patternElements.length - 1 || !(e.kind === SyntaxKind.BindingElement && (<BindingElement>e).dotDotDotToken || e.kind === SyntaxKind.SpreadElement)) {
17222+
if (e.kind !== SyntaxKind.OmittedExpression) {
17223+
error(e, Diagnostics.Initializer_provides_no_value_for_this_binding_element_and_the_binding_element_has_no_default_value);
17224+
}
17225+
elementTypes.push(strictNullChecks ? implicitNeverType : undefinedWideningType);
17226+
}
17227+
}
17228+
}
17229+
return createTupleType(elementTypes, minLength, hasRestElement);
17230+
}
17231+
}
17232+
1722517233
function getArrayLiteralType(elementTypes: Type[], unionReduction = UnionReduction.Literal) {
1722617234
return createArrayType(elementTypes.length ?
1722717235
getUnionType(elementTypes, unionReduction) :
@@ -17637,11 +17645,13 @@ namespace ts {
1763717645
error(attributes, Diagnostics._0_are_specified_twice_The_attribute_named_0_will_be_overwritten, unescapeLeadingUnderscores(jsxChildrenPropertyName));
1763817646
}
1763917647

17648+
const contextualType = getApparentTypeOfContextualType(openingLikeElement.attributes);
17649+
const childrenContextualType = contextualType && getTypeOfPropertyOfContextualType(contextualType, jsxChildrenPropertyName);
1764017650
// If there are children in the body of JSX element, create dummy attribute "children" with the union of children types so that it will pass the attribute checking process
1764117651
const childrenPropSymbol = createSymbol(SymbolFlags.Property | SymbolFlags.Transient, jsxChildrenPropertyName);
1764217652
childrenPropSymbol.type = childrenTypes.length === 1 ?
1764317653
childrenTypes[0] :
17644-
createArrayType(getUnionType(childrenTypes));
17654+
(getArrayLiteralTupleTypeIfApplicable(childrenTypes, childrenContextualType, /*hasRestElement*/ false) || createArrayType(getUnionType(childrenTypes)));
1764517655
const childPropMap = createSymbolTable();
1764617656
childPropMap.set(jsxChildrenPropertyName, childrenPropSymbol);
1764717657
spread = getSpreadType(spread, createAnonymousType(attributes.symbol, childPropMap, emptyArray, emptyArray, /*stringIndexInfo*/ undefined, /*numberIndexInfo*/ undefined),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
tests/cases/conformance/jsx/checkJsxChildrenCanBeTupleType.tsx(17,18): error TS2322: Type '{ children: [Element, Element, Element]; }' is not assignable to type 'Readonly<ResizablePanelProps>'.
2+
Types of property 'children' are incompatible.
3+
Type '[Element, Element, Element]' is not assignable to type '[ReactNode, ReactNode]'.
4+
Types of property 'length' are incompatible.
5+
Type '3' is not assignable to type '2'.
6+
7+
8+
==== tests/cases/conformance/jsx/checkJsxChildrenCanBeTupleType.tsx (1 errors) ====
9+
/// <reference path="/.lib/react16.d.ts" />
10+
11+
import React from 'react'
12+
13+
interface ResizablePanelProps {
14+
children: [React.ReactNode, React.ReactNode]
15+
}
16+
17+
class ResizablePanel extends React.Component<
18+
ResizablePanelProps, any> {}
19+
20+
const test = <ResizablePanel>
21+
<div />
22+
<div />
23+
</ResizablePanel>
24+
25+
const testErr = <ResizablePanel>
26+
~~~~~~~~~~~~~~
27+
!!! error TS2322: Type '{ children: [Element, Element, Element]; }' is not assignable to type 'Readonly<ResizablePanelProps>'.
28+
!!! error TS2322: Types of property 'children' are incompatible.
29+
!!! error TS2322: Type '[Element, Element, Element]' is not assignable to type '[ReactNode, ReactNode]'.
30+
!!! error TS2322: Types of property 'length' are incompatible.
31+
!!! error TS2322: Type '3' is not assignable to type '2'.
32+
<div />
33+
<div />
34+
<div />
35+
</ResizablePanel>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//// [checkJsxChildrenCanBeTupleType.tsx]
2+
/// <reference path="/.lib/react16.d.ts" />
3+
4+
import React from 'react'
5+
6+
interface ResizablePanelProps {
7+
children: [React.ReactNode, React.ReactNode]
8+
}
9+
10+
class ResizablePanel extends React.Component<
11+
ResizablePanelProps, any> {}
12+
13+
const test = <ResizablePanel>
14+
<div />
15+
<div />
16+
</ResizablePanel>
17+
18+
const testErr = <ResizablePanel>
19+
<div />
20+
<div />
21+
<div />
22+
</ResizablePanel>
23+
24+
//// [checkJsxChildrenCanBeTupleType.js]
25+
"use strict";
26+
/// <reference path="react16.d.ts" />
27+
var __extends = (this && this.__extends) || (function () {
28+
var extendStatics = function (d, b) {
29+
extendStatics = Object.setPrototypeOf ||
30+
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
31+
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
32+
return extendStatics(d, b);
33+
}
34+
return function (d, b) {
35+
extendStatics(d, b);
36+
function __() { this.constructor = d; }
37+
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
38+
};
39+
})();
40+
var __importDefault = (this && this.__importDefault) || function (mod) {
41+
return (mod && mod.__esModule) ? mod : { "default": mod };
42+
};
43+
exports.__esModule = true;
44+
var react_1 = __importDefault(require("react"));
45+
var ResizablePanel = /** @class */ (function (_super) {
46+
__extends(ResizablePanel, _super);
47+
function ResizablePanel() {
48+
return _super !== null && _super.apply(this, arguments) || this;
49+
}
50+
return ResizablePanel;
51+
}(react_1["default"].Component));
52+
var test = react_1["default"].createElement(ResizablePanel, null,
53+
react_1["default"].createElement("div", null),
54+
react_1["default"].createElement("div", null));
55+
var testErr = react_1["default"].createElement(ResizablePanel, null,
56+
react_1["default"].createElement("div", null),
57+
react_1["default"].createElement("div", null),
58+
react_1["default"].createElement("div", null));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
=== tests/cases/conformance/jsx/checkJsxChildrenCanBeTupleType.tsx ===
2+
/// <reference path="react16.d.ts" />
3+
4+
import React from 'react'
5+
>React : Symbol(React, Decl(checkJsxChildrenCanBeTupleType.tsx, 2, 6))
6+
7+
interface ResizablePanelProps {
8+
>ResizablePanelProps : Symbol(ResizablePanelProps, Decl(checkJsxChildrenCanBeTupleType.tsx, 2, 25))
9+
10+
children: [React.ReactNode, React.ReactNode]
11+
>children : Symbol(ResizablePanelProps.children, Decl(checkJsxChildrenCanBeTupleType.tsx, 4, 31))
12+
>React : Symbol(React, Decl(checkJsxChildrenCanBeTupleType.tsx, 2, 6))
13+
>ReactNode : Symbol(React.ReactNode, Decl(react16.d.ts, 216, 49))
14+
>React : Symbol(React, Decl(checkJsxChildrenCanBeTupleType.tsx, 2, 6))
15+
>ReactNode : Symbol(React.ReactNode, Decl(react16.d.ts, 216, 49))
16+
}
17+
18+
class ResizablePanel extends React.Component<
19+
>ResizablePanel : Symbol(ResizablePanel, Decl(checkJsxChildrenCanBeTupleType.tsx, 6, 1))
20+
>React.Component : Symbol(React.Component, Decl(react16.d.ts, 345, 54), Decl(react16.d.ts, 349, 94))
21+
>React : Symbol(React, Decl(checkJsxChildrenCanBeTupleType.tsx, 2, 6))
22+
>Component : Symbol(React.Component, Decl(react16.d.ts, 345, 54), Decl(react16.d.ts, 349, 94))
23+
24+
ResizablePanelProps, any> {}
25+
>ResizablePanelProps : Symbol(ResizablePanelProps, Decl(checkJsxChildrenCanBeTupleType.tsx, 2, 25))
26+
27+
const test = <ResizablePanel>
28+
>test : Symbol(test, Decl(checkJsxChildrenCanBeTupleType.tsx, 11, 5))
29+
>ResizablePanel : Symbol(ResizablePanel, Decl(checkJsxChildrenCanBeTupleType.tsx, 6, 1))
30+
31+
<div />
32+
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2420, 114))
33+
34+
<div />
35+
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2420, 114))
36+
37+
</ResizablePanel>
38+
>ResizablePanel : Symbol(ResizablePanel, Decl(checkJsxChildrenCanBeTupleType.tsx, 6, 1))
39+
40+
const testErr = <ResizablePanel>
41+
>testErr : Symbol(testErr, Decl(checkJsxChildrenCanBeTupleType.tsx, 16, 5))
42+
>ResizablePanel : Symbol(ResizablePanel, Decl(checkJsxChildrenCanBeTupleType.tsx, 6, 1))
43+
44+
<div />
45+
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2420, 114))
46+
47+
<div />
48+
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2420, 114))
49+
50+
<div />
51+
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2420, 114))
52+
53+
</ResizablePanel>
54+
>ResizablePanel : Symbol(ResizablePanel, Decl(checkJsxChildrenCanBeTupleType.tsx, 6, 1))
55+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
=== tests/cases/conformance/jsx/checkJsxChildrenCanBeTupleType.tsx ===
2+
/// <reference path="react16.d.ts" />
3+
4+
import React from 'react'
5+
>React : typeof React
6+
7+
interface ResizablePanelProps {
8+
children: [React.ReactNode, React.ReactNode]
9+
>children : [React.ReactNode, React.ReactNode]
10+
>React : any
11+
>React : any
12+
}
13+
14+
class ResizablePanel extends React.Component<
15+
>ResizablePanel : ResizablePanel
16+
>React.Component : React.Component<ResizablePanelProps, any, any>
17+
>React : typeof React
18+
>Component : typeof React.Component
19+
20+
ResizablePanelProps, any> {}
21+
22+
const test = <ResizablePanel>
23+
>test : JSX.Element
24+
><ResizablePanel> <div /> <div /></ResizablePanel> : JSX.Element
25+
>ResizablePanel : typeof ResizablePanel
26+
27+
<div />
28+
><div /> : JSX.Element
29+
>div : any
30+
31+
<div />
32+
><div /> : JSX.Element
33+
>div : any
34+
35+
</ResizablePanel>
36+
>ResizablePanel : typeof ResizablePanel
37+
38+
const testErr = <ResizablePanel>
39+
>testErr : JSX.Element
40+
><ResizablePanel> <div /> <div /> <div /></ResizablePanel> : JSX.Element
41+
>ResizablePanel : typeof ResizablePanel
42+
43+
<div />
44+
><div /> : JSX.Element
45+
>div : any
46+
47+
<div />
48+
><div /> : JSX.Element
49+
>div : any
50+
51+
<div />
52+
><div /> : JSX.Element
53+
>div : any
54+
55+
</ResizablePanel>
56+
>ResizablePanel : typeof ResizablePanel
57+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// @jsx: react
2+
// @strict: true
3+
// @esModuleInterop: true
4+
/// <reference path="/.lib/react16.d.ts" />
5+
6+
import React from 'react'
7+
8+
interface ResizablePanelProps {
9+
children: [React.ReactNode, React.ReactNode]
10+
}
11+
12+
class ResizablePanel extends React.Component<
13+
ResizablePanelProps, any> {}
14+
15+
const test = <ResizablePanel>
16+
<div />
17+
<div />
18+
</ResizablePanel>
19+
20+
const testErr = <ResizablePanel>
21+
<div />
22+
<div />
23+
<div />
24+
</ResizablePanel>

0 commit comments

Comments
 (0)