diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 480f7189fce..9bafc0c9838 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -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; diff --git a/tests/baselines/reference/jsxFragmentChildrenCheck.js b/tests/baselines/reference/jsxFragmentChildrenCheck.js new file mode 100644 index 00000000000..5beaff1ef55 --- /dev/null +++ b/tests/baselines/reference/jsxFragmentChildrenCheck.js @@ -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 {} + +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 + + {"ok"} + {({ foo }) => "also ok"} +; + +// 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"; + }] }); diff --git a/tests/baselines/reference/jsxFragmentChildrenCheck.symbols b/tests/baselines/reference/jsxFragmentChildrenCheck.symbols new file mode 100644 index 00000000000..df5e6a1c145 --- /dev/null +++ b/tests/baselines/reference/jsxFragmentChildrenCheck.symbols @@ -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 {} +>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 : Symbol(Fragment, Decl(index.tsx, 0, 8)) + + {"ok"} + {({ foo }) => "also ok"} +>foo : Symbol(foo, Decl(index.tsx, 5, 5)) + +; +>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)) + +; + diff --git a/tests/baselines/reference/jsxFragmentChildrenCheck.types b/tests/baselines/reference/jsxFragmentChildrenCheck.types new file mode 100644 index 00000000000..baa622a8127 --- /dev/null +++ b/tests/baselines/reference/jsxFragmentChildrenCheck.types @@ -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 {} + +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 + +> {"ok"} {({ foo }) => "also ok"} : 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 : (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" +> : ^^^^^^^^^^^^^^^^^^^ + +; + diff --git a/tests/cases/compiler/jsxFragmentChildrenCheck.tsx b/tests/cases/compiler/jsxFragmentChildrenCheck.tsx new file mode 100644 index 00000000000..d2907476456 --- /dev/null +++ b/tests/cases/compiler/jsxFragmentChildrenCheck.tsx @@ -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 {} + +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 + + {"ok"} + {({ foo }) => "also ok"} +; + +// This should also pass - using <> syntax should be equivalent +<> + {"ok"} + {({ foo }) => "should also be ok"} +;