diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 4fa3dcf560c..28592568ea7 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -14766,11 +14766,11 @@ namespace ts { signatures = mapDefined(signatures, s => getJsxSignatureTypeArgumentInstantiation(s, context, isJs)); } - return getUnionType(map(signatures, ctor ? t => getJsxPropsTypeFromConstructSignature(t, isJs, context) : t => getJsxPropsTypeFromCallSignature(t, context)), UnionReduction.None); + return getUnionType(map(signatures, ctor ? t => getJsxPropsTypeFromClassType(t, isJs, context, /*reportErrors*/ false) : t => getJsxPropsTypeFromCallSignature(t, context)), UnionReduction.None); } function getJsxPropsTypeFromCallSignature(sig: Signature, context: Node) { - let propsType = getTypeOfFirstParameterOfSignature(sig); + let propsType = getTypeOfFirstParameterOfSignatureWithFallback(sig, emptyObjectType); const intrinsicAttribs = getJsxType(JsxNames.IntrinsicAttributes, context); if (intrinsicAttribs !== unknownType) { propsType = intersectTypes(intrinsicAttribs, propsType); @@ -14778,68 +14778,57 @@ namespace ts { return propsType; } - function getJsxPropsTypeFromClassType(hostClassType: Type, isJs: boolean, context: JsxOpeningLikeElement, reportErrors: boolean) { - if (isTypeAny(hostClassType)) { - return hostClassType; - } + function getJsxPropsTypeForSignatureFromMember(sig: Signature, forcedLookupLocation: __String) { + const instanceType = getReturnTypeOfSignature(sig); + return isTypeAny(instanceType) ? instanceType : getTypeOfPropertyOfType(instanceType, forcedLookupLocation); + } - const propsName = getJsxElementPropertiesName(getJsxNamespaceAt(context)); - if (propsName === undefined) { - // There is no type ElementAttributesProperty, return 'any' - return anyType; + function getJsxPropsTypeFromClassType(sig: Signature, isJs: boolean, context: JsxOpeningLikeElement, reportErrors: boolean) { + const forcedLookupLocation = getJsxElementPropertiesName(getJsxNamespaceAt(context)); + const attributesType = forcedLookupLocation === undefined + // If there is no type ElementAttributesProperty, return the type of the first parameter of the signature, which should be the props type + ? getTypeOfFirstParameterOfSignatureWithFallback(sig, emptyObjectType) + : forcedLookupLocation === "" + // If there is no e.g. 'props' member in ElementAttributesProperty, use the element class type instead + ? getReturnTypeOfSignature(sig) + // Otherwise get the type of the property on the signature return type + : getJsxPropsTypeForSignatureFromMember(sig, forcedLookupLocation); + + if (!attributesType) { + // There is no property named 'props' on this instance type + if (reportErrors && !!forcedLookupLocation && !!length(context.attributes.properties)) { + error(context, Diagnostics.JSX_element_class_does_not_support_attributes_because_it_does_not_have_a_0_property, unescapeLeadingUnderscores(forcedLookupLocation)); + } + return emptyObjectType; } - else if (propsName === "") { - // If there is no e.g. 'props' member in ElementAttributesProperty, use the element class type instead - return hostClassType; + else if (isTypeAny(attributesType)) { + // Props is of type 'any' or unknown + return attributesType; } else { - const attributesType = getTypeOfPropertyOfType(hostClassType, propsName); - - if (!attributesType) { - // There is no property named 'props' on this instance type - if (reportErrors && !!length(context.attributes.properties)) { - error(context, Diagnostics.JSX_element_class_does_not_support_attributes_because_it_does_not_have_a_0_property, unescapeLeadingUnderscores(propsName)); - } - return emptyObjectType; + // Normal case -- add in IntrinsicClassElements and IntrinsicElements + let apparentAttributesType = attributesType; + const intrinsicClassAttribs = getJsxType(JsxNames.IntrinsicClassAttributes, context); + if (intrinsicClassAttribs !== unknownType) { + const typeParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(intrinsicClassAttribs.symbol); + const hostClassType = getReturnTypeOfSignature(sig); + apparentAttributesType = intersectTypes( + typeParams + ? createTypeReference(intrinsicClassAttribs, fillMissingTypeArguments([hostClassType], typeParams, getMinTypeArgumentCount(typeParams), isJs)) + : intrinsicClassAttribs, + apparentAttributesType + ); } - else if (isTypeAny(attributesType)) { - // Props is of type 'any' or unknown - return attributesType; - } - else { - // Normal case -- add in IntrinsicClassElements and IntrinsicElements - let apparentAttributesType = attributesType; - const intrinsicClassAttribs = getJsxType(JsxNames.IntrinsicClassAttributes, context); - if (intrinsicClassAttribs !== unknownType) { - const typeParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(intrinsicClassAttribs.symbol); - apparentAttributesType = intersectTypes( - typeParams - ? createTypeReference(intrinsicClassAttribs, fillMissingTypeArguments([hostClassType], typeParams, getMinTypeArgumentCount(typeParams), isJs)) - : intrinsicClassAttribs, - apparentAttributesType - ); - } - const intrinsicAttribs = getJsxType(JsxNames.IntrinsicAttributes, context); - if (intrinsicAttribs !== unknownType) { - apparentAttributesType = intersectTypes(intrinsicAttribs, apparentAttributesType); - } - - return apparentAttributesType; + const intrinsicAttribs = getJsxType(JsxNames.IntrinsicAttributes, context); + if (intrinsicAttribs !== unknownType) { + apparentAttributesType = intersectTypes(intrinsicAttribs, apparentAttributesType); } + + return apparentAttributesType; } } - function getJsxPropsTypeFromConstructSignature(sig: Signature, isJs: boolean, context: JsxOpeningLikeElement) { - const hostClassType = getReturnTypeOfSignature(sig); - if (hostClassType) { - return getJsxPropsTypeFromClassType(hostClassType, isJs, context, /*reportErrors*/ false); - } - return getJsxPropsTypeFromCallSignature(sig, context); - } - - - // If the given type is an object or union type with a single signature, and if that signature has at // least as many parameters as the given function, return the signature. Otherwise return undefined. function getContextualCallSignature(type: Type, node: FunctionExpression | ArrowFunction | MethodDeclaration): Signature { @@ -15556,31 +15545,7 @@ namespace ts { return links.resolvedSymbol; } - /** - * Given a JSX element that is a class element, finds the Element Instance Type. If the - * element is not a class element, or the class element type cannot be determined, returns 'undefined'. - * For example, in the element , the element instance type is `MyClass` (not `typeof MyClass`). - */ - function getJsxElementInstanceType(node: JsxOpeningLikeElement, valueType: Type) { - Debug.assert(!(valueType.flags & TypeFlags.Union)); - if (isTypeAny(valueType)) { - // Short-circuit if the class tag is using an element type 'any' - return anyType; - } - - // Resolve the signatures, preferring constructor - let signatures = getSignaturesOfType(valueType, SignatureKind.Construct); - if (signatures.length === 0) { - // No construct signatures, try call signatures - signatures = getSignaturesOfType(valueType, SignatureKind.Call); - if (signatures.length === 0) { - // We found no signatures at all, which is an error - error(node.tagName, Diagnostics.JSX_element_type_0_does_not_have_any_construct_or_call_signatures, getTextOfNode(node.tagName)); - return unknownType; - } - } - - // Instantiate in context of source type + function instantiateJsxSignatures(node: JsxOpeningLikeElement, signatures: Signature[]) { const instantiatedSignatures = []; let candidateForTypeArgumentError: Signature; let hasTypeArgumentError: boolean = !!node.typeArguments; @@ -15605,7 +15570,6 @@ namespace ts { instantiatedSignatures.push(signature); } } - if (node.typeArguments && hasTypeArgumentError) { if (candidateForTypeArgumentError) { checkTypeArguments(candidateForTypeArgumentError, node.typeArguments, /*reportErrors*/ true); @@ -15615,8 +15579,7 @@ namespace ts { diagnostics.add(getTypeArgumentArityError(node, signatures, node.typeArguments)); } } - - return getUnionType(map(instantiatedSignatures, getReturnTypeOfSignature), UnionReduction.Subtype); + return instantiatedSignatures; } function getJsxSignatureTypeArgumentInstantiation(signature: Signature, node: JsxOpeningLikeElement, isJavascript: boolean, reportErrors?: boolean) { @@ -15827,8 +15790,12 @@ namespace ts { }), UnionReduction.Subtype); } + // Shortcircuit any + if (isTypeAny(elementType)) { + return elementType; + } // If the elemType is a string type, we have to return anyType to prevent an error downstream as we will try to find construct or call signature of the type - if (elementType.flags & TypeFlags.String) { + else if (elementType.flags & TypeFlags.String) { return anyType; } else if (elementType.flags & TypeFlags.StringLiteral) { @@ -15854,7 +15821,22 @@ namespace ts { } // Get the element instance type (the result of newing or invoking this tag) - const elemInstanceType = getJsxElementInstanceType(openingLikeElement, elementType); + + // Resolve the signatures, preferring constructor + let signatures = getSignaturesOfType(elementType, SignatureKind.Construct); + if (signatures.length === 0) { + // No construct signatures, try call signatures + signatures = getSignaturesOfType(elementType, SignatureKind.Call); + if (signatures.length === 0) { + // We found no signatures at all, which is an error + error(openingLikeElement.tagName, Diagnostics.JSX_element_type_0_does_not_have_any_construct_or_call_signatures, getTextOfNode(openingLikeElement.tagName)); + return unknownType; + } + } + + // Instantiate in context of source type + const instantiatedSignatures = instantiateJsxSignatures(openingLikeElement, signatures); + const elemInstanceType = getUnionType(map(instantiatedSignatures, getReturnTypeOfSignature), UnionReduction.Subtype); // If we should include all stateless attributes type, then get all attributes type from all stateless function signature. // Otherwise get only attributes type from the signature picked by choose-overload logic. @@ -15871,7 +15853,8 @@ namespace ts { checkTypeRelatedTo(elemInstanceType, elementClassType, assignableRelation, openingLikeElement, Diagnostics.JSX_element_type_0_is_not_a_constructor_function_for_JSX_elements); } - return getJsxPropsTypeFromClassType(elemInstanceType, isInJavaScriptFile(openingLikeElement), openingLikeElement, /*reportErrors*/ true); + const isJs = isInJavaScriptFile(openingLikeElement); + return getUnionType(map(instantiatedSignatures, sig => getJsxPropsTypeFromClassType(sig, isJs, openingLikeElement, /*reportErrors*/ true))); } /** @@ -18469,8 +18452,13 @@ namespace ts { pos < signature.parameters.length ? getTypeOfParameter(signature.parameters[pos]) : anyType; } + function getTypeOfFirstParameterOfSignature(signature: Signature) { - return signature.parameters.length > 0 ? getTypeAtPosition(signature, 0) : neverType; + return getTypeOfFirstParameterOfSignatureWithFallback(signature, neverType); + } + + function getTypeOfFirstParameterOfSignatureWithFallback(signature: Signature, fallbackType: Type) { + return signature.parameters.length > 0 ? getTypeAtPosition(signature, 0) : fallbackType; } function inferFromAnnotatedParameters(signature: Signature, context: Signature, mapper: TypeMapper) { @@ -27565,7 +27553,7 @@ namespace ts { export const JSX = "JSX" as __String; export const IntrinsicElements = "IntrinsicElements" as __String; export const ElementClass = "ElementClass" as __String; - export const ElementAttributesPropertyNameContainer = "ElementAttributesProperty" as __String; + export const ElementAttributesPropertyNameContainer = "ElementAttributesProperty" as __String; // TODO: Deprecate and remove support export const ElementChildrenAttributeNameContainer = "ElementChildrenAttribute" as __String; export const Element = "Element" as __String; export const IntrinsicAttributes = "IntrinsicAttributes" as __String; diff --git a/tests/baselines/reference/tsxElementResolution10.errors.txt b/tests/baselines/reference/tsxElementResolution10.errors.txt index 9214ffe7dbb..3e59030460f 100644 --- a/tests/baselines/reference/tsxElementResolution10.errors.txt +++ b/tests/baselines/reference/tsxElementResolution10.errors.txt @@ -1,8 +1,10 @@ tests/cases/conformance/jsx/file.tsx(13,1): error TS2605: JSX element type '{ x: number; }' is not a constructor function for JSX elements. Property 'render' is missing in type '{ x: number; }'. +tests/cases/conformance/jsx/file.tsx(13,7): error TS2322: Type '{ x: number; }' is not assignable to type 'string'. +tests/cases/conformance/jsx/file.tsx(19,7): error TS2322: Type '{ x: number; render: number; }' is not assignable to type 'string'. -==== tests/cases/conformance/jsx/file.tsx (1 errors) ==== +==== tests/cases/conformance/jsx/file.tsx (3 errors) ==== declare module JSX { interface Element { } interface ElementClass { @@ -19,10 +21,14 @@ tests/cases/conformance/jsx/file.tsx(13,1): error TS2605: JSX element type '{ x: ~~~~~~~~~~~~~~~ !!! error TS2605: JSX element type '{ x: number; }' is not a constructor function for JSX elements. !!! error TS2605: Property 'render' is missing in type '{ x: number; }'. + ~~~~~~ +!!! error TS2322: Type '{ x: number; }' is not assignable to type 'string'. interface Obj2type { (n: string): { x: number; render: any; }; } var Obj2: Obj2type; ; // OK + ~~~~~~~~~~~~~~~~~~~ +!!! error TS2322: Type '{ x: number; render: number; }' is not assignable to type 'string'. \ No newline at end of file diff --git a/tests/baselines/reference/tsxElementResolution15.errors.txt b/tests/baselines/reference/tsxElementResolution15.errors.txt index 35126294f67..9f065482ef4 100644 --- a/tests/baselines/reference/tsxElementResolution15.errors.txt +++ b/tests/baselines/reference/tsxElementResolution15.errors.txt @@ -1,7 +1,8 @@ tests/cases/conformance/jsx/file.tsx(3,12): error TS2608: The global type 'JSX.ElementAttributesProperty' may not have more than one property. +tests/cases/conformance/jsx/file.tsx(11,7): error TS2322: Type '{ x: number; }' is not assignable to type 'string'. -==== tests/cases/conformance/jsx/file.tsx (1 errors) ==== +==== tests/cases/conformance/jsx/file.tsx (2 errors) ==== declare module JSX { interface Element { } interface ElementAttributesProperty { pr1: any; pr2: any; } @@ -15,4 +16,6 @@ tests/cases/conformance/jsx/file.tsx(3,12): error TS2608: The global type 'JSX.E } var Obj1: Obj1type; ; // Error + ~~~~~~ +!!! error TS2322: Type '{ x: number; }' is not assignable to type 'string'. \ No newline at end of file diff --git a/tests/baselines/reference/tsxElementResolution9.errors.txt b/tests/baselines/reference/tsxElementResolution9.errors.txt new file mode 100644 index 00000000000..c5c4e211877 --- /dev/null +++ b/tests/baselines/reference/tsxElementResolution9.errors.txt @@ -0,0 +1,44 @@ +tests/cases/conformance/jsx/file.tsx(11,1): error TS2322: Type '{}' is not assignable to type 'string | number'. + Type '{}' is not assignable to type 'number'. +tests/cases/conformance/jsx/file.tsx(18,1): error TS2322: Type '{}' is not assignable to type 'string | number'. + Type '{}' is not assignable to type 'number'. +tests/cases/conformance/jsx/file.tsx(25,7): error TS2322: Type '{ x: number; }' is not assignable to type 'string | number'. + Type '{ x: number; }' is not assignable to type 'number'. + + +==== tests/cases/conformance/jsx/file.tsx (3 errors) ==== + declare module JSX { + interface Element { something; } + interface IntrinsicElements { } + } + + interface Obj1 { + new(n: string): { x: number }; + new(n: number): { y: string }; + } + var Obj1: Obj1; + ; // Error, return type is not an object type + ~~~~~~~~ +!!! error TS2322: Type '{}' is not assignable to type 'string | number'. +!!! error TS2322: Type '{}' is not assignable to type 'number'. + + interface Obj2 { + (n: string): { x: number }; + (n: number): { y: string }; + } + var Obj2: Obj2; + ; // Error, return type is not an object type + ~~~~~~~~ +!!! error TS2322: Type '{}' is not assignable to type 'string | number'. +!!! error TS2322: Type '{}' is not assignable to type 'number'. + + interface Obj3 { + (n: string): { x: number }; + (n: number): { x: number; y: string }; + } + var Obj3: Obj3; + ; // OK + ~~~~~~ +!!! error TS2322: Type '{ x: number; }' is not assignable to type 'string | number'. +!!! error TS2322: Type '{ x: number; }' is not assignable to type 'number'. + \ No newline at end of file diff --git a/tests/baselines/reference/tsxTypeArgumentPartialDefinitionStillErrors.errors.txt b/tests/baselines/reference/tsxTypeArgumentPartialDefinitionStillErrors.errors.txt new file mode 100644 index 00000000000..18f896107b3 --- /dev/null +++ b/tests/baselines/reference/tsxTypeArgumentPartialDefinitionStillErrors.errors.txt @@ -0,0 +1,22 @@ +tests/cases/compiler/file.tsx(11,14): error TS2322: Type '{ prop: number; }' is not assignable to type 'Record'. + Property 'prop' is incompatible with index signature. + Type 'number' is not assignable to type 'string'. + + +==== tests/cases/compiler/file.tsx (1 errors) ==== + declare namespace JSX { + interface Element { + render(): Element | string | false; + } + } + + function SFC(props: Record) { + return ''; + } + + prop={1}>; // should error + ~~~~~~~~ +!!! error TS2322: Type '{ prop: number; }' is not assignable to type 'Record'. +!!! error TS2322: Property 'prop' is incompatible with index signature. +!!! error TS2322: Type 'number' is not assignable to type 'string'. + \ No newline at end of file diff --git a/tests/baselines/reference/tsxTypeArgumentPartialDefinitionStillErrors.js b/tests/baselines/reference/tsxTypeArgumentPartialDefinitionStillErrors.js new file mode 100644 index 00000000000..03d71852e1e --- /dev/null +++ b/tests/baselines/reference/tsxTypeArgumentPartialDefinitionStillErrors.js @@ -0,0 +1,19 @@ +//// [file.tsx] +declare namespace JSX { + interface Element { + render(): Element | string | false; + } +} + +function SFC(props: Record) { + return ''; +} + + prop={1}>; // should error + + +//// [file.jsx] +function SFC(props) { + return ''; +} +; // should error diff --git a/tests/baselines/reference/tsxTypeArgumentPartialDefinitionStillErrors.symbols b/tests/baselines/reference/tsxTypeArgumentPartialDefinitionStillErrors.symbols new file mode 100644 index 00000000000..b0c5502e9b7 --- /dev/null +++ b/tests/baselines/reference/tsxTypeArgumentPartialDefinitionStillErrors.symbols @@ -0,0 +1,28 @@ +=== tests/cases/compiler/file.tsx === +declare namespace JSX { +>JSX : Symbol(JSX, Decl(file.tsx, 0, 0)) + + interface Element { +>Element : Symbol(Element, Decl(file.tsx, 0, 23)) + + render(): Element | string | false; +>render : Symbol(Element.render, Decl(file.tsx, 1, 23)) +>Element : Symbol(Element, Decl(file.tsx, 0, 23)) + } +} + +function SFC(props: Record) { +>SFC : Symbol(SFC, Decl(file.tsx, 4, 1)) +>T : Symbol(T, Decl(file.tsx, 6, 13)) +>props : Symbol(props, Decl(file.tsx, 6, 16)) +>Record : Symbol(Record, Decl(lib.d.ts, --, --)) +>T : Symbol(T, Decl(file.tsx, 6, 13)) + + return ''; +} + + prop={1}>; // should error +>SFC : Symbol(SFC, Decl(file.tsx, 4, 1)) +>prop : Symbol(prop, Decl(file.tsx, 10, 12)) +>SFC : Symbol(SFC, Decl(file.tsx, 4, 1)) + diff --git a/tests/baselines/reference/tsxTypeArgumentPartialDefinitionStillErrors.types b/tests/baselines/reference/tsxTypeArgumentPartialDefinitionStillErrors.types new file mode 100644 index 00000000000..be854eb53fc --- /dev/null +++ b/tests/baselines/reference/tsxTypeArgumentPartialDefinitionStillErrors.types @@ -0,0 +1,32 @@ +=== tests/cases/compiler/file.tsx === +declare namespace JSX { +>JSX : any + + interface Element { +>Element : Element + + render(): Element | string | false; +>render : () => string | false | Element +>Element : Element +>false : false + } +} + +function SFC(props: Record) { +>SFC : (props: Record) => string +>T : T +>props : Record +>Record : Record +>T : T + + return ''; +>'' : "" +} + + prop={1}>; // should error +> prop={1}> : JSX.Element +>SFC : (props: Record) => string +>prop : number +>1 : 1 +>SFC : (props: Record) => string + diff --git a/tests/baselines/reference/tsxTypeErrors.errors.txt b/tests/baselines/reference/tsxTypeErrors.errors.txt new file mode 100644 index 00000000000..bab5a5c671b --- /dev/null +++ b/tests/baselines/reference/tsxTypeErrors.errors.txt @@ -0,0 +1,42 @@ +tests/cases/conformance/jsx/tsxTypeErrors.tsx(25,19): error TS2339: Property 'reqd' does not exist on type '{}'. +tests/cases/conformance/jsx/tsxTypeErrors.tsx(31,19): error TS2339: Property 'pt' does not exist on type '{}'. + + +==== tests/cases/conformance/jsx/tsxTypeErrors.tsx (2 errors) ==== + // A built-in element (OK) + var a1 =
; + + // A built-in element with a mistyped property (error) + var a2 = + + // A built-in element with a badly-typed attribute value (error) + var thing = { oops: 100 }; + var a3 =
+ + // Mistyped html name (error) + var e1 = + + // A custom type + class MyClass { + props: { + pt?: { x: number; y: number; }; + name?: string; + reqd: boolean; + } + } + + // Let's use it + // TODO: Error on missing 'reqd' + var b1 = ; + ~~~~~~~~~~~ +!!! error TS2339: Property 'reqd' does not exist on type '{}'. + + // Mistyped attribute member + // sample.tsx(23,22): error TS2322: Type '{ x: number; y: string; }' is not assignable to type '{ x: number; y: number; }'. + // Types of property 'y' are incompatible. + // Type 'string' is not assignable to type 'number'. + var b2 = ; + ~~~~~~~~~~~~~~~~~~~~~~ +!!! error TS2339: Property 'pt' does not exist on type '{}'. + + \ No newline at end of file diff --git a/tests/cases/compiler/tsxTypeArgumentPartialDefinitionStillErrors.ts b/tests/cases/compiler/tsxTypeArgumentPartialDefinitionStillErrors.ts new file mode 100644 index 00000000000..db8d7c3cd0c --- /dev/null +++ b/tests/cases/compiler/tsxTypeArgumentPartialDefinitionStillErrors.ts @@ -0,0 +1,13 @@ +// @jsx: preserve +// @filename: file.tsx +declare namespace JSX { + interface Element { + render(): Element | string | false; + } +} + +function SFC(props: Record) { + return ''; +} + + prop={1}>; // should error