From 2cbfb51ebbbda20ff4623fd83d86f47254466062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 26 Apr 2023 22:39:08 +0200 Subject: [PATCH] Fixed JSX attributes discriminating based on optional children (#53980) --- src/compiler/checker.ts | 12 +- .../contextuallyTypedJsxChildren.symbols | 114 ++++++++++++++++++ .../contextuallyTypedJsxChildren.types | 106 ++++++++++++++++ .../compiler/contextuallyTypedJsxChildren.tsx | 47 ++++++++ 4 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 tests/baselines/reference/contextuallyTypedJsxChildren.symbols create mode 100644 tests/baselines/reference/contextuallyTypedJsxChildren.types create mode 100644 tests/cases/compiler/contextuallyTypedJsxChildren.tsx diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 406ff8715d1..cc906abfc21 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -29336,6 +29336,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } function discriminateContextualTypeByJSXAttributes(node: JsxAttributes, contextualType: UnionType) { + const jsxChildrenPropertyName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node)); return discriminateTypeByDiscriminableItems(contextualType, concatenate( map( @@ -29343,7 +29344,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { prop => ([!(prop as JsxAttribute).initializer ? (() => trueType) : (() => getContextFreeTypeOfExpression((prop as JsxAttribute).initializer!)), prop.symbol.escapedName] as [() => Type, __String]) ), map( - filter(getPropertiesOfType(contextualType), s => !!(s.flags & SymbolFlags.Optional) && !!node?.symbol?.members && !node.symbol.members.has(s.escapedName) && isDiscriminantProperty(contextualType, s.escapedName)), + filter(getPropertiesOfType(contextualType), s => { + if (!(s.flags & SymbolFlags.Optional) || !node?.symbol?.members) { + return false; + } + const element = node.parent.parent; + if (s.escapedName === jsxChildrenPropertyName && isJsxElement(element) && getSemanticJsxChildren(element.children).length) { + return false; + } + return !node.symbol.members.has(s.escapedName) && isDiscriminantProperty(contextualType, s.escapedName); + }), s => [() => undefinedType, s.escapedName] as [() => Type, __String] ) ), diff --git a/tests/baselines/reference/contextuallyTypedJsxChildren.symbols b/tests/baselines/reference/contextuallyTypedJsxChildren.symbols new file mode 100644 index 00000000000..f93d70ccf76 --- /dev/null +++ b/tests/baselines/reference/contextuallyTypedJsxChildren.symbols @@ -0,0 +1,114 @@ +=== tests/cases/compiler/contextuallyTypedJsxChildren.tsx === +/// + +import React from 'react'; +>React : Symbol(React, Decl(contextuallyTypedJsxChildren.tsx, 2, 6)) + +// repro from https://github.com/microsoft/TypeScript/issues/53941 +declare namespace DropdownMenu { +>DropdownMenu : Symbol(DropdownMenu, Decl(contextuallyTypedJsxChildren.tsx, 2, 26), Decl(contextuallyTypedJsxChildren.tsx, 23, 13)) + + interface BaseProps { +>BaseProps : Symbol(BaseProps, Decl(contextuallyTypedJsxChildren.tsx, 5, 32)) + + icon: string; +>icon : Symbol(BaseProps.icon, Decl(contextuallyTypedJsxChildren.tsx, 6, 23)) + + label: string; +>label : Symbol(BaseProps.label, Decl(contextuallyTypedJsxChildren.tsx, 7, 17)) + } + interface PropsWithChildren extends BaseProps { +>PropsWithChildren : Symbol(PropsWithChildren, Decl(contextuallyTypedJsxChildren.tsx, 9, 3)) +>BaseProps : Symbol(BaseProps, Decl(contextuallyTypedJsxChildren.tsx, 5, 32)) + + children(props: { onClose: () => void }): JSX.Element; +>children : Symbol(PropsWithChildren.children, Decl(contextuallyTypedJsxChildren.tsx, 10, 49)) +>props : Symbol(props, Decl(contextuallyTypedJsxChildren.tsx, 11, 13)) +>onClose : Symbol(onClose, Decl(contextuallyTypedJsxChildren.tsx, 11, 21)) +>JSX : Symbol(JSX, Decl(react16.d.ts, 2493, 12)) +>Element : Symbol(JSX.Element, Decl(react16.d.ts, 2494, 23)) + + controls?: never | undefined; +>controls : Symbol(PropsWithChildren.controls, Decl(contextuallyTypedJsxChildren.tsx, 11, 58)) + } + interface PropsWithControls extends BaseProps { +>PropsWithControls : Symbol(PropsWithControls, Decl(contextuallyTypedJsxChildren.tsx, 13, 3)) +>BaseProps : Symbol(BaseProps, Decl(contextuallyTypedJsxChildren.tsx, 5, 32)) + + controls: Control[]; +>controls : Symbol(PropsWithControls.controls, Decl(contextuallyTypedJsxChildren.tsx, 14, 49)) +>Control : Symbol(Control, Decl(contextuallyTypedJsxChildren.tsx, 17, 3)) + + children?: never | undefined; +>children : Symbol(PropsWithControls.children, Decl(contextuallyTypedJsxChildren.tsx, 15, 24)) + } + interface Control { +>Control : Symbol(Control, Decl(contextuallyTypedJsxChildren.tsx, 17, 3)) + + title: string; +>title : Symbol(Control.title, Decl(contextuallyTypedJsxChildren.tsx, 18, 21)) + } + type Props = PropsWithChildren | PropsWithControls; +>Props : Symbol(Props, Decl(contextuallyTypedJsxChildren.tsx, 20, 3)) +>PropsWithChildren : Symbol(PropsWithChildren, Decl(contextuallyTypedJsxChildren.tsx, 9, 3)) +>PropsWithControls : Symbol(PropsWithControls, Decl(contextuallyTypedJsxChildren.tsx, 13, 3)) +} +declare const DropdownMenu: React.ComponentType; +>DropdownMenu : Symbol(DropdownMenu, Decl(contextuallyTypedJsxChildren.tsx, 2, 26), Decl(contextuallyTypedJsxChildren.tsx, 23, 13)) +>React : Symbol(React, Decl(contextuallyTypedJsxChildren.tsx, 2, 6)) +>ComponentType : Symbol(React.ComponentType, Decl(react16.d.ts, 117, 60)) +>DropdownMenu : Symbol(DropdownMenu, Decl(contextuallyTypedJsxChildren.tsx, 2, 26), Decl(contextuallyTypedJsxChildren.tsx, 23, 13)) +>Props : Symbol(DropdownMenu.Props, Decl(contextuallyTypedJsxChildren.tsx, 20, 3)) + + +>DropdownMenu : Symbol(DropdownMenu, Decl(contextuallyTypedJsxChildren.tsx, 2, 26), Decl(contextuallyTypedJsxChildren.tsx, 23, 13)) +>icon : Symbol(icon, Decl(contextuallyTypedJsxChildren.tsx, 25, 13)) +>label : Symbol(label, Decl(contextuallyTypedJsxChildren.tsx, 25, 25)) + + {({ onClose }) => ( +>onClose : Symbol(onClose, Decl(contextuallyTypedJsxChildren.tsx, 26, 5)) + +
+>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114)) + + +>button : Symbol(JSX.IntrinsicElements.button, Decl(react16.d.ts, 2532, 96)) +>onClick : Symbol(onClick, Decl(contextuallyTypedJsxChildren.tsx, 28, 13)) +>onClose : Symbol(onClose, Decl(contextuallyTypedJsxChildren.tsx, 26, 5)) +>button : Symbol(JSX.IntrinsicElements.button, Decl(react16.d.ts, 2532, 96)) + +
+>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114)) + + )} +
; +>DropdownMenu : Symbol(DropdownMenu, Decl(contextuallyTypedJsxChildren.tsx, 2, 26), Decl(contextuallyTypedJsxChildren.tsx, 23, 13)) + +DropdownMenu : Symbol(DropdownMenu, Decl(contextuallyTypedJsxChildren.tsx, 2, 26), Decl(contextuallyTypedJsxChildren.tsx, 23, 13)) + + icon="move" +>icon : Symbol(icon, Decl(contextuallyTypedJsxChildren.tsx, 33, 13)) + + label="Select a direction" +>label : Symbol(label, Decl(contextuallyTypedJsxChildren.tsx, 34, 13)) + + children={({ onClose }) => ( +>children : Symbol(children, Decl(contextuallyTypedJsxChildren.tsx, 35, 28)) +>onClose : Symbol(onClose, Decl(contextuallyTypedJsxChildren.tsx, 36, 14)) + +
+>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114)) + + +>button : Symbol(JSX.IntrinsicElements.button, Decl(react16.d.ts, 2532, 96)) +>onClick : Symbol(onClick, Decl(contextuallyTypedJsxChildren.tsx, 38, 13)) +>onClose : Symbol(onClose, Decl(contextuallyTypedJsxChildren.tsx, 36, 14)) +>button : Symbol(JSX.IntrinsicElements.button, Decl(react16.d.ts, 2532, 96)) + +
+>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114)) + + )} +/>; + diff --git a/tests/baselines/reference/contextuallyTypedJsxChildren.types b/tests/baselines/reference/contextuallyTypedJsxChildren.types new file mode 100644 index 00000000000..4b1e13dfa56 --- /dev/null +++ b/tests/baselines/reference/contextuallyTypedJsxChildren.types @@ -0,0 +1,106 @@ +=== tests/cases/compiler/contextuallyTypedJsxChildren.tsx === +/// + +import React from 'react'; +>React : typeof React + +// repro from https://github.com/microsoft/TypeScript/issues/53941 +declare namespace DropdownMenu { + interface BaseProps { + icon: string; +>icon : string + + label: string; +>label : string + } + interface PropsWithChildren extends BaseProps { + children(props: { onClose: () => void }): JSX.Element; +>children : (props: { onClose: () => void;}) => JSX.Element +>props : { onClose: () => void; } +>onClose : () => void +>JSX : any + + controls?: never | undefined; +>controls : undefined + } + interface PropsWithControls extends BaseProps { + controls: Control[]; +>controls : Control[] + + children?: never | undefined; +>children : undefined + } + interface Control { + title: string; +>title : string + } + type Props = PropsWithChildren | PropsWithControls; +>Props : PropsWithChildren | PropsWithControls +} +declare const DropdownMenu: React.ComponentType; +>DropdownMenu : React.ComponentType +>React : any +>DropdownMenu : any + + +> {({ onClose }) => (
)}
: JSX.Element +>DropdownMenu : React.ComponentType +>icon : string +>label : string + + {({ onClose }) => ( +>({ onClose }) => (
) : ({ onClose }: { onClose: () => void; }) => JSX.Element +>onClose : () => void +>(
) : JSX.Element + +
+>
: JSX.Element +>div : any + + +> : JSX.Element +>button : any +>onClick : () => void +>onClose : () => void +>button : any + +
+>div : any + + )} +
; +>DropdownMenu : React.ComponentType + + (
)}/> : JSX.Element +>DropdownMenu : React.ComponentType + + icon="move" +>icon : string + + label="Select a direction" +>label : string + + children={({ onClose }) => ( +>children : ({ onClose }: { onClose: () => void; }) => JSX.Element +>({ onClose }) => (
) : ({ onClose }: { onClose: () => void; }) => JSX.Element +>onClose : () => void +>(
) : JSX.Element + +
+>
: JSX.Element +>div : any + + +> : JSX.Element +>button : any +>onClick : () => void +>onClose : () => void +>button : any + +
+>div : any + + )} +/>; + diff --git a/tests/cases/compiler/contextuallyTypedJsxChildren.tsx b/tests/cases/compiler/contextuallyTypedJsxChildren.tsx new file mode 100644 index 00000000000..bbc1e7ff3bf --- /dev/null +++ b/tests/cases/compiler/contextuallyTypedJsxChildren.tsx @@ -0,0 +1,47 @@ +// @strict: true +// @jsx: react +// @esModuleInterop: true +// @noEmit: true + +/// + +import React from 'react'; + +// repro from https://github.com/microsoft/TypeScript/issues/53941 +declare namespace DropdownMenu { + interface BaseProps { + icon: string; + label: string; + } + interface PropsWithChildren extends BaseProps { + children(props: { onClose: () => void }): JSX.Element; + controls?: never | undefined; + } + interface PropsWithControls extends BaseProps { + controls: Control[]; + children?: never | undefined; + } + interface Control { + title: string; + } + type Props = PropsWithChildren | PropsWithControls; +} +declare const DropdownMenu: React.ComponentType; + + + {({ onClose }) => ( +
+ +
+ )} +
; + + ( +
+ +
+ )} +/>;