Add test case and implement fix for JSX Fragment typing with react-jsx mode

Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-11-10 16:12:35 +00:00
parent 607b949970
commit 483093f36f
5 changed files with 308 additions and 7 deletions
+29 -7
View File
@@ -33791,7 +33791,26 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
(isJsxElement(parent) && parent.openingElement === openingLikeElement || isJsxFragment(parent) && parent.openingFragment === openingLikeElement) &&
getSemanticJsxChildren(parent.children).length > 0
) {
const childrenTypes: Type[] = checkJsxChildren(parent, checkMode);
// Compute contextual type for children before checking them
let childrenContextualType: Type | undefined;
if (jsxChildrenPropertyName && jsxChildrenPropertyName !== "") {
let contextualType: Type | undefined;
if (isJsxOpeningElement(openingLikeElement)) {
contextualType = getApparentTypeOfContextualType(openingLikeElement.attributes, /*contextFlags*/ undefined);
}
else if (isJsxOpeningFragment(openingLikeElement)) {
// For fragments, get the props type from the Fragment factory's signature
const fragmentType = getJSXFragmentType(openingLikeElement);
const signatures = getSignaturesOfType(fragmentType, SignatureKind.Call);
if (signatures.length > 0) {
contextualType = getTypeOfFirstParameterOfSignature(signatures[0]);
}
}
childrenContextualType = contextualType && getTypeOfPropertyOfContextualType(contextualType, jsxChildrenPropertyName);
}
// Check children with contextual type
const childrenTypes: Type[] = checkJsxChildren(parent, checkMode, childrenContextualType);
if (!hasSpreadAnyType && jsxChildrenPropertyName && jsxChildrenPropertyName !== "") {
// Error if there is a attribute named "children" explicitly specified and children element.
@@ -33800,9 +33819,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (explicitlySpecifyChildrenAttribute) {
error(attributeParent, Diagnostics._0_are_specified_twice_The_attribute_named_0_will_be_overwritten, unescapeLeadingUnderscores(jsxChildrenPropertyName));
}
const contextualType = isJsxOpeningElement(openingLikeElement) ? getApparentTypeOfContextualType(openingLikeElement.attributes, /*contextFlags*/ undefined) : undefined;
const childrenContextualType = contextualType && getTypeOfPropertyOfContextualType(contextualType, jsxChildrenPropertyName);
// 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
const childrenPropSymbol = createSymbol(SymbolFlags.Property, jsxChildrenPropertyName);
childrenPropSymbol.links.type = childrenTypes.length === 1 ? childrenTypes[0] :
@@ -33842,7 +33858,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return result;
}
function checkJsxChildren(node: JsxElement | JsxFragment, checkMode?: CheckMode) {
function checkJsxChildren(node: JsxElement | JsxFragment, checkMode?: CheckMode, childrenContextualType?: Type) {
const childrenTypes: Type[] = [];
for (const child of node.children) {
// In React, JSX text that contains only whitespaces will be ignored so we don't want to type-check that
@@ -33856,7 +33872,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
continue; // empty jsx expressions don't *really* count as present children
}
else {
childrenTypes.push(checkExpressionForMutableLocation(child, checkMode));
// If we have a contextual type for children, use it when checking child expressions
if (childrenContextualType && child.kind === SyntaxKind.JsxExpression && child.expression) {
childrenTypes.push(checkExpressionForMutableLocationWithContextualType(child.expression, childrenContextualType));
}
else {
childrenTypes.push(checkExpressionForMutableLocation(child, checkMode));
}
}
}
return childrenTypes;
@@ -37418,7 +37440,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const jsxFragmentFactoryName = getJsxNamespace(node);
// #38720/60122, allow null as jsxFragmentFactory
const shouldResolveFactoryReference = (compilerOptions.jsx === JsxEmit.React || compilerOptions.jsxFragmentFactory !== undefined) && jsxFragmentFactoryName !== "null";
const shouldResolveFactoryReference = (compilerOptions.jsx === JsxEmit.React || compilerOptions.jsx === JsxEmit.ReactJSX || compilerOptions.jsx === JsxEmit.ReactJSXDev || compilerOptions.jsxFragmentFactory !== undefined) && jsxFragmentFactoryName !== "null";
if (!shouldResolveFactoryReference) return sourceFileLinks.jsxFragmentType = anyType;
const shouldModuleRefErr = compilerOptions.jsx !== JsxEmit.Preserve && compilerOptions.jsx !== JsxEmit.ReactNative;
@@ -0,0 +1,57 @@
//// [tests/cases/compiler/jsxFragmentChildrenCheck.tsx] ////
//// [index.d.ts]
export const jsx: any;
export const jsxs: any;
type JsxElement =
| JsxElementArray
| undefined
| string
| ((arg: { foo: "bar" }) => void);
interface JsxElementArray extends Array<JsxElement> {}
interface FragmentProps {
children?: JsxElement;
}
export const Fragment: (props: FragmentProps) => any;
declare global {
namespace JSX {
interface IntrinsicElements {
div: any;
span: any;
}
}
}
//// [index.tsx]
import { Fragment } from "@test/jsx-runtime";
// This should pass - using explicit Fragment
<Fragment>
{"ok"}
{({ foo }) => "also ok"}
</Fragment>;
// This should also pass - using <> syntax should be equivalent
<>
{"ok"}
{({ foo }) => "should also be ok"}
</>;
//// [index.js]
import { jsxs as _jsxs, Fragment as _Fragment } from "@test/jsx-runtime";
import { Fragment } from "@test/jsx-runtime";
// This should pass - using explicit Fragment
_jsxs(Fragment, { children: ["ok", function (_a) {
var foo = _a.foo;
return "also ok";
}] });
// This should also pass - using <> syntax should be equivalent
_jsxs(_Fragment, { children: ["ok", function (_a) {
var foo = _a.foo;
return "should also be ok";
}] });
@@ -0,0 +1,80 @@
//// [tests/cases/compiler/jsxFragmentChildrenCheck.tsx] ////
=== node_modules/@test/jsx-runtime/index.d.ts ===
export const jsx: any;
>jsx : Symbol(jsx, Decl(index.d.ts, 0, 12))
export const jsxs: any;
>jsxs : Symbol(jsxs, Decl(index.d.ts, 1, 12))
type JsxElement =
>JsxElement : Symbol(JsxElement, Decl(index.d.ts, 1, 23))
| JsxElementArray
>JsxElementArray : Symbol(JsxElementArray, Decl(index.d.ts, 7, 36))
| undefined
| string
| ((arg: { foo: "bar" }) => void);
>arg : Symbol(arg, Decl(index.d.ts, 7, 6))
>foo : Symbol(foo, Decl(index.d.ts, 7, 12))
interface JsxElementArray extends Array<JsxElement> {}
>JsxElementArray : Symbol(JsxElementArray, Decl(index.d.ts, 7, 36))
>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --))
>JsxElement : Symbol(JsxElement, Decl(index.d.ts, 1, 23))
interface FragmentProps {
>FragmentProps : Symbol(FragmentProps, Decl(index.d.ts, 8, 54))
children?: JsxElement;
>children : Symbol(FragmentProps.children, Decl(index.d.ts, 10, 25))
>JsxElement : Symbol(JsxElement, Decl(index.d.ts, 1, 23))
}
export const Fragment: (props: FragmentProps) => any;
>Fragment : Symbol(Fragment, Decl(index.d.ts, 14, 12))
>props : Symbol(props, Decl(index.d.ts, 14, 24))
>FragmentProps : Symbol(FragmentProps, Decl(index.d.ts, 8, 54))
declare global {
>global : Symbol(global, Decl(index.d.ts, 14, 53))
namespace JSX {
>JSX : Symbol(JSX, Decl(index.d.ts, 16, 16))
interface IntrinsicElements {
>IntrinsicElements : Symbol(IntrinsicElements, Decl(index.d.ts, 17, 17))
div: any;
>div : Symbol(IntrinsicElements.div, Decl(index.d.ts, 18, 33))
span: any;
>span : Symbol(IntrinsicElements.span, Decl(index.d.ts, 19, 15))
}
}
}
=== index.tsx ===
import { Fragment } from "@test/jsx-runtime";
>Fragment : Symbol(Fragment, Decl(index.tsx, 0, 8))
// This should pass - using explicit Fragment
<Fragment>
>Fragment : Symbol(Fragment, Decl(index.tsx, 0, 8))
{"ok"}
{({ foo }) => "also ok"}
>foo : Symbol(foo, Decl(index.tsx, 5, 5))
</Fragment>;
>Fragment : Symbol(Fragment, Decl(index.tsx, 0, 8))
// This should also pass - using <> syntax should be equivalent
<>
{"ok"}
{({ foo }) => "should also be ok"}
>foo : Symbol(foo, Decl(index.tsx, 11, 5))
</>;
@@ -0,0 +1,96 @@
//// [tests/cases/compiler/jsxFragmentChildrenCheck.tsx] ////
=== node_modules/@test/jsx-runtime/index.d.ts ===
export const jsx: any;
>jsx : any
export const jsxs: any;
>jsxs : any
type JsxElement =
>JsxElement : JsxElement
> : ^^^^^^^^^^
| JsxElementArray
| undefined
| string
| ((arg: { foo: "bar" }) => void);
>arg : { foo: "bar"; }
> : ^^^^^^^ ^^^
>foo : "bar"
> : ^^^^^
interface JsxElementArray extends Array<JsxElement> {}
interface FragmentProps {
children?: JsxElement;
>children : JsxElement
> : ^^^^^^^^^^
}
export const Fragment: (props: FragmentProps) => any;
>Fragment : (props: FragmentProps) => any
> : ^ ^^ ^^^^^
>props : FragmentProps
> : ^^^^^^^^^^^^^
declare global {
>global : any
> : ^^^
namespace JSX {
interface IntrinsicElements {
div: any;
>div : any
span: any;
>span : any
}
}
}
=== index.tsx ===
import { Fragment } from "@test/jsx-runtime";
>Fragment : (props: import("node_modules/@test/jsx-runtime/index").FragmentProps) => any
> : ^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
// This should pass - using explicit Fragment
<Fragment>
><Fragment> {"ok"} {({ foo }) => "also ok"}</Fragment> : error
>Fragment : (props: import("node_modules/@test/jsx-runtime/index").FragmentProps) => any
> : ^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
{"ok"}
>"ok" : "ok"
> : ^^^^
{({ foo }) => "also ok"}
>({ foo }) => "also ok" : ({ foo }: { foo: "bar"; }) => string
> : ^ ^^^^^^^^^ ^^^^^^^^^^^^^^
>foo : "bar"
> : ^^^^^
>"also ok" : "also ok"
> : ^^^^^^^^^
</Fragment>;
>Fragment : (props: import("node_modules/@test/jsx-runtime/index").FragmentProps) => any
> : ^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
// This should also pass - using <> syntax should be equivalent
<>
><> {"ok"} {({ foo }) => "should also be ok"}</> : any
{"ok"}
>"ok" : "ok"
> : ^^^^
{({ foo }) => "should also be ok"}
>({ foo }) => "should also be ok" : ({ foo }: { foo: "bar"; }) => string
> : ^ ^^^^^^^^^ ^^^^^^^^^^^^^^
>foo : "bar"
> : ^^^^^
>"should also be ok" : "should also be ok"
> : ^^^^^^^^^^^^^^^^^^^
</>;
@@ -0,0 +1,46 @@
// @jsx: react-jsx
// @strict: true
// @module: esnext
// @skipLibCheck: true
// @jsxImportSource: @test
// @filename: node_modules/@test/jsx-runtime/index.d.ts
export const jsx: any;
export const jsxs: any;
type JsxElement =
| JsxElementArray
| undefined
| string
| ((arg: { foo: "bar" }) => void);
interface JsxElementArray extends Array<JsxElement> {}
interface FragmentProps {
children?: JsxElement;
}
export const Fragment: (props: FragmentProps) => any;
declare global {
namespace JSX {
interface IntrinsicElements {
div: any;
span: any;
}
}
}
// @filename: index.tsx
import { Fragment } from "@test/jsx-runtime";
// This should pass - using explicit Fragment
<Fragment>
{"ok"}
{({ foo }) => "also ok"}
</Fragment>;
// This should also pass - using <> syntax should be equivalent
<>
{"ok"}
{({ foo }) => "should also be ok"}
</>;