Skip to content

Commit

Permalink
Improve error message for invalid return type of JSX component (#32702)
Browse files Browse the repository at this point in the history
* New diagnostic message for wrong JSX function component

* Component and Mixed type

* fix existing tests

* add new test for JSX component return type error

* fix tslint error

* update diagnostic message to include component name

* accept baseline

* update tests

* missing semicolon

* accept baseline

Co-authored-by: Wesley Wigham <wwigham@gmail.com>
  • Loading branch information
uhyo and weswigham authored Mar 30, 2020
1 parent 4c440e5 commit 1f56ab0
Show file tree
Hide file tree
Showing 12 changed files with 509 additions and 47 deletions.
20 changes: 13 additions & 7 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23356,18 +23356,18 @@ namespace ts {
return anyType;
}

function checkJsxReturnAssignableToAppropriateBound(refKind: JsxReferenceKind, elemInstanceType: Type, openingLikeElement: Node) {
function checkJsxReturnAssignableToAppropriateBound(refKind: JsxReferenceKind, elemInstanceType: Type, openingLikeElement: JsxOpeningLikeElement) {
if (refKind === JsxReferenceKind.Function) {
const sfcReturnConstraint = getJsxStatelessElementTypeAt(openingLikeElement);
if (sfcReturnConstraint) {
checkTypeRelatedTo(elemInstanceType, sfcReturnConstraint, assignableRelation, openingLikeElement, Diagnostics.JSX_element_type_0_is_not_a_constructor_function_for_JSX_elements);
checkTypeRelatedTo(elemInstanceType, sfcReturnConstraint, assignableRelation, openingLikeElement.tagName, Diagnostics.Its_return_type_0_is_not_a_valid_JSX_element, generateInitialErrorChain);
}
}
else if (refKind === JsxReferenceKind.Component) {
const classConstraint = getJsxElementClassTypeAt(openingLikeElement);
if (classConstraint) {
// Issue an error if this return type isn't assignable to JSX.ElementClass or JSX.Element, failing that
checkTypeRelatedTo(elemInstanceType, classConstraint, assignableRelation, openingLikeElement, Diagnostics.JSX_element_type_0_is_not_a_constructor_function_for_JSX_elements);
// Issue an error if this return type isn't assignable to JSX.ElementClass, failing that
checkTypeRelatedTo(elemInstanceType, classConstraint, assignableRelation, openingLikeElement.tagName, Diagnostics.Its_instance_type_0_is_not_a_valid_JSX_element, generateInitialErrorChain);
}
}
else { // Mixed
Expand All @@ -23377,7 +23377,12 @@ namespace ts {
return;
}
const combined = getUnionType([sfcReturnConstraint, classConstraint]);
checkTypeRelatedTo(elemInstanceType, combined, assignableRelation, openingLikeElement, Diagnostics.JSX_element_type_0_is_not_a_constructor_function_for_JSX_elements);
checkTypeRelatedTo(elemInstanceType, combined, assignableRelation, openingLikeElement.tagName, Diagnostics.Its_element_type_0_is_not_a_valid_JSX_element, generateInitialErrorChain);
}

function generateInitialErrorChain(): DiagnosticMessageChain {
const componentName = getTextOfNode(openingLikeElement.tagName);
return chainDiagnosticMessages(/* details */ undefined, Diagnostics._0_cannot_be_used_as_a_JSX_component, componentName);
}
}

Expand Down Expand Up @@ -23468,8 +23473,9 @@ namespace ts {
}

if (isNodeOpeningLikeElement) {
const sig = getResolvedSignature(node as JsxOpeningLikeElement);
checkJsxReturnAssignableToAppropriateBound(getJsxReferenceKind(node as JsxOpeningLikeElement), getReturnTypeOfSignature(sig), node);
const jsxOpeningLikeNode = node as JsxOpeningLikeElement;
const sig = getResolvedSignature(jsxOpeningLikeNode);
checkJsxReturnAssignableToAppropriateBound(getJsxReferenceKind(jsxOpeningLikeNode), getReturnTypeOfSignature(sig), jsxOpeningLikeNode);
}
}

Expand Down
16 changes: 16 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2947,6 +2947,22 @@
"category": "Error",
"code": 2785
},
"'{0}' cannot be used as a JSX component.": {
"category": "Error",
"code": 2786
},
"Its return type '{0}' is not a valid JSX element.": {
"category": "Error",
"code": 2787
},
"Its instance type '{0}' is not a valid JSX element.": {
"category": "Error",
"code": 2788
},
"Its element type '{0}' is not a valid JSX element.": {
"category": "Error",
"code": 2789
},

"Import declaration '{0}' is using private name '{1}'.": {
"category": "Error",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
tests/cases/conformance/jsx/inline/index.tsx(5,1): error TS2741: Property '__predomBrand' is missing in type 'import("tests/cases/conformance/jsx/inline/renderer").dom.JSX.Element' but required in type 'import("tests/cases/conformance/jsx/inline/renderer2").predom.JSX.Element'.
tests/cases/conformance/jsx/inline/index.tsx(21,21): error TS2605: JSX element type 'import("tests/cases/conformance/jsx/inline/renderer2").predom.JSX.Element' is not a constructor function for JSX elements.
Property '__domBrand' is missing in type 'import("tests/cases/conformance/jsx/inline/renderer2").predom.JSX.Element' but required in type 'import("tests/cases/conformance/jsx/inline/renderer").dom.JSX.Element'.
tests/cases/conformance/jsx/inline/index.tsx(21,22): error TS2786: 'MySFC' cannot be used as a JSX component.
Its return type 'import("tests/cases/conformance/jsx/inline/renderer2").predom.JSX.Element' is not a valid JSX element.
Property '__domBrand' is missing in type 'import("tests/cases/conformance/jsx/inline/renderer2").predom.JSX.Element' but required in type 'import("tests/cases/conformance/jsx/inline/renderer").dom.JSX.Element'.
tests/cases/conformance/jsx/inline/index.tsx(21,40): error TS2322: Type 'import("tests/cases/conformance/jsx/inline/renderer").dom.JSX.Element' is not assignable to type 'import("tests/cases/conformance/jsx/inline/renderer2").predom.JSX.Element'.
tests/cases/conformance/jsx/inline/index.tsx(21,40): error TS2605: JSX element type 'MyClass' is not a constructor function for JSX elements.
Property '__domBrand' is missing in type 'MyClass' but required in type 'ElementClass'.
tests/cases/conformance/jsx/inline/index.tsx(21,41): error TS2786: 'MyClass' cannot be used as a JSX component.
Its instance type 'MyClass' is not a valid JSX element.
Property '__domBrand' is missing in type 'MyClass' but required in type 'ElementClass'.
tests/cases/conformance/jsx/inline/index.tsx(21,63): error TS2322: Type 'import("tests/cases/conformance/jsx/inline/renderer").dom.JSX.Element' is not assignable to type 'import("tests/cases/conformance/jsx/inline/renderer2").predom.JSX.Element'.
tests/cases/conformance/jsx/inline/index.tsx(21,63): error TS2605: JSX element type 'MyClass' is not a constructor function for JSX elements.
tests/cases/conformance/jsx/inline/index.tsx(21,64): error TS2786: 'MyClass' cannot be used as a JSX component.
Its instance type 'MyClass' is not a valid JSX element.
tests/cases/conformance/jsx/inline/index.tsx(24,42): error TS2322: Type 'import("tests/cases/conformance/jsx/inline/renderer2").predom.JSX.Element' is not assignable to type 'import("tests/cases/conformance/jsx/inline/renderer").dom.JSX.Element'.
tests/cases/conformance/jsx/inline/index.tsx(24,48): error TS2322: Type 'import("tests/cases/conformance/jsx/inline/renderer2").predom.JSX.Element' is not assignable to type 'import("tests/cases/conformance/jsx/inline/renderer").dom.JSX.Element'.

Expand Down Expand Up @@ -95,20 +98,23 @@ tests/cases/conformance/jsx/inline/index.tsx(24,48): error TS2322: Type 'import(

// Should fail, no dom elements
const _brokenTree = <MySFC x={1} y={2}><MyClass x={3} y={4} /><MyClass x={5} y={6} /></MySFC>
~~~~~~~~~~~~~~~~~~~
!!! error TS2605: JSX element type 'import("tests/cases/conformance/jsx/inline/renderer2").predom.JSX.Element' is not a constructor function for JSX elements.
!!! error TS2605: Property '__domBrand' is missing in type 'import("tests/cases/conformance/jsx/inline/renderer2").predom.JSX.Element' but required in type 'import("tests/cases/conformance/jsx/inline/renderer").dom.JSX.Element'.
~~~~~
!!! error TS2786: 'MySFC' cannot be used as a JSX component.
!!! error TS2786: Its return type 'import("tests/cases/conformance/jsx/inline/renderer2").predom.JSX.Element' is not a valid JSX element.
!!! error TS2786: Property '__domBrand' is missing in type 'import("tests/cases/conformance/jsx/inline/renderer2").predom.JSX.Element' but required in type 'import("tests/cases/conformance/jsx/inline/renderer").dom.JSX.Element'.
!!! related TS2728 tests/cases/conformance/jsx/inline/renderer.d.ts:7:13: '__domBrand' is declared here.
~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2322: Type 'import("tests/cases/conformance/jsx/inline/renderer").dom.JSX.Element' is not assignable to type 'import("tests/cases/conformance/jsx/inline/renderer2").predom.JSX.Element'.
~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2605: JSX element type 'MyClass' is not a constructor function for JSX elements.
!!! error TS2605: Property '__domBrand' is missing in type 'MyClass' but required in type 'ElementClass'.
~~~~~~~
!!! error TS2786: 'MyClass' cannot be used as a JSX component.
!!! error TS2786: Its instance type 'MyClass' is not a valid JSX element.
!!! error TS2786: Property '__domBrand' is missing in type 'MyClass' but required in type 'ElementClass'.
!!! related TS2728 tests/cases/conformance/jsx/inline/renderer.d.ts:7:13: '__domBrand' is declared here.
~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2322: Type 'import("tests/cases/conformance/jsx/inline/renderer").dom.JSX.Element' is not assignable to type 'import("tests/cases/conformance/jsx/inline/renderer2").predom.JSX.Element'.
~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2605: JSX element type 'MyClass' is not a constructor function for JSX elements.
~~~~~~~
!!! error TS2786: 'MyClass' cannot be used as a JSX component.
!!! error TS2786: Its instance type 'MyClass' is not a valid JSX element.

// Should fail, nondom isn't allowed as children of dom
const _brokenTree2 = <DOMSFC x={1} y={2}>{tree}{tree}</DOMSFC>
Expand Down
103 changes: 103 additions & 0 deletions tests/baselines/reference/jsxComponentTypeErrors.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
tests/cases/compiler/jsxComponentTypeErrors.tsx(16,11): error TS2786: 'this' cannot be used as a JSX component.
Its return type '{ type: "foo" | undefined; }' is not a valid JSX element.
Types of property 'type' are incompatible.
Type '"foo" | undefined' is not assignable to type '"element"'.
Type 'undefined' is not assignable to type '"element"'.
tests/cases/compiler/jsxComponentTypeErrors.tsx(25,16): error TS2786: 'FunctionComponent' cannot be used as a JSX component.
Its return type '{ type: "abc" | undefined; }' is not a valid JSX element.
Types of property 'type' are incompatible.
Type '"abc" | undefined' is not assignable to type '"element"'.
Type 'undefined' is not assignable to type '"element"'.
tests/cases/compiler/jsxComponentTypeErrors.tsx(26,16): error TS2786: 'FunctionComponent' cannot be used as a JSX component.
Its return type '{ type: "abc" | undefined; }' is not a valid JSX element.
tests/cases/compiler/jsxComponentTypeErrors.tsx(27,16): error TS2786: 'ClassComponent' cannot be used as a JSX component.
Its instance type 'ClassComponent' is not a valid JSX element.
Types of property 'type' are incompatible.
Type 'string' is not assignable to type '"element-class"'.
tests/cases/compiler/jsxComponentTypeErrors.tsx(28,16): error TS2786: 'MixedComponent' cannot be used as a JSX component.
Its element type 'ClassComponent | { type: string | undefined; }' is not a valid JSX element.
Type 'ClassComponent' is not assignable to type 'Element | ElementClass | null'.
Type 'ClassComponent' is not assignable to type 'ElementClass'.
tests/cases/compiler/jsxComponentTypeErrors.tsx(37,16): error TS2786: 'obj.MemberFunctionComponent' cannot be used as a JSX component.
Its return type '{}' is not a valid JSX element.
Property 'type' is missing in type '{}' but required in type 'Element'.
tests/cases/compiler/jsxComponentTypeErrors.tsx(38,16): error TS2786: 'obj. MemberClassComponent' cannot be used as a JSX component.
Its instance type 'MemberClassComponent' is not a valid JSX element.
Property 'type' is missing in type 'MemberClassComponent' but required in type 'ElementClass'.


==== tests/cases/compiler/jsxComponentTypeErrors.tsx (7 errors) ====
namespace JSX {
export interface Element {
type: 'element';
}
export interface ElementClass {
type: 'element-class';
}
}

function FunctionComponent<T extends string>({type}: {type?: T}) {
return {
type
}
}
FunctionComponent.useThis = function() {
return <this type="foo" />;
~~~~
!!! error TS2786: 'this' cannot be used as a JSX component.
!!! error TS2786: Its return type '{ type: "foo" | undefined; }' is not a valid JSX element.
!!! error TS2786: Types of property 'type' are incompatible.
!!! error TS2786: Type '"foo" | undefined' is not assignable to type '"element"'.
!!! error TS2786: Type 'undefined' is not assignable to type '"element"'.
}

class ClassComponent {
type = 'string';
}

const MixedComponent = Math.random() ? FunctionComponent : ClassComponent;

const elem1 = <FunctionComponent type="abc" />;
~~~~~~~~~~~~~~~~~
!!! error TS2786: 'FunctionComponent' cannot be used as a JSX component.
!!! error TS2786: Its return type '{ type: "abc" | undefined; }' is not a valid JSX element.
!!! error TS2786: Types of property 'type' are incompatible.
!!! error TS2786: Type '"abc" | undefined' is not assignable to type '"element"'.
!!! error TS2786: Type 'undefined' is not assignable to type '"element"'.
const elem2 = <FunctionComponent<"abc"> />;
~~~~~~~~~~~~~~~~~
!!! error TS2786: 'FunctionComponent' cannot be used as a JSX component.
!!! error TS2786: Its return type '{ type: "abc" | undefined; }' is not a valid JSX element.
const elem3 = <ClassComponent />;
~~~~~~~~~~~~~~
!!! error TS2786: 'ClassComponent' cannot be used as a JSX component.
!!! error TS2786: Its instance type 'ClassComponent' is not a valid JSX element.
!!! error TS2786: Types of property 'type' are incompatible.
!!! error TS2786: Type 'string' is not assignable to type '"element-class"'.
const elem4 = <MixedComponent />;
~~~~~~~~~~~~~~
!!! error TS2786: 'MixedComponent' cannot be used as a JSX component.
!!! error TS2786: Its element type 'ClassComponent | { type: string | undefined; }' is not a valid JSX element.
!!! error TS2786: Type 'ClassComponent' is not assignable to type 'Element | ElementClass | null'.
!!! error TS2786: Type 'ClassComponent' is not assignable to type 'ElementClass'.

const obj = {
MemberFunctionComponent() {
return {};
},
MemberClassComponent: class {},
};

const elem5 = <obj.MemberFunctionComponent />;
~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2786: 'obj.MemberFunctionComponent' cannot be used as a JSX component.
!!! error TS2786: Its return type '{}' is not a valid JSX element.
!!! error TS2786: Property 'type' is missing in type '{}' but required in type 'Element'.
!!! related TS2728 tests/cases/compiler/jsxComponentTypeErrors.tsx:3:5: 'type' is declared here.
const elem6 = <obj. MemberClassComponent />;
~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2786: 'obj. MemberClassComponent' cannot be used as a JSX component.
!!! error TS2786: Its instance type 'MemberClassComponent' is not a valid JSX element.
!!! error TS2786: Property 'type' is missing in type 'MemberClassComponent' but required in type 'ElementClass'.
!!! related TS2728 tests/cases/compiler/jsxComponentTypeErrors.tsx:6:5: 'type' is declared here.

75 changes: 75 additions & 0 deletions tests/baselines/reference/jsxComponentTypeErrors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//// [jsxComponentTypeErrors.tsx]
namespace JSX {
export interface Element {
type: 'element';
}
export interface ElementClass {
type: 'element-class';
}
}

function FunctionComponent<T extends string>({type}: {type?: T}) {
return {
type
}
}
FunctionComponent.useThis = function() {
return <this type="foo" />;
}

class ClassComponent {
type = 'string';
}

const MixedComponent = Math.random() ? FunctionComponent : ClassComponent;

const elem1 = <FunctionComponent type="abc" />;
const elem2 = <FunctionComponent<"abc"> />;
const elem3 = <ClassComponent />;
const elem4 = <MixedComponent />;

const obj = {
MemberFunctionComponent() {
return {};
},
MemberClassComponent: class {},
};

const elem5 = <obj.MemberFunctionComponent />;
const elem6 = <obj. MemberClassComponent />;


//// [jsxComponentTypeErrors.jsx]
"use strict";
function FunctionComponent(_a) {
var type = _a.type;
return {
type: type
};
}
FunctionComponent.useThis = function () {
return <this type="foo"/>;
};
var ClassComponent = /** @class */ (function () {
function ClassComponent() {
this.type = 'string';
}
return ClassComponent;
}());
var MixedComponent = Math.random() ? FunctionComponent : ClassComponent;
var elem1 = <FunctionComponent type="abc"/>;
var elem2 = <FunctionComponent />;
var elem3 = <ClassComponent />;
var elem4 = <MixedComponent />;
var obj = {
MemberFunctionComponent: function () {
return {};
},
MemberClassComponent: /** @class */ (function () {
function MemberClassComponent() {
}
return MemberClassComponent;
}())
};
var elem5 = <obj.MemberFunctionComponent />;
var elem6 = <obj.MemberClassComponent />;
Loading

0 comments on commit 1f56ab0

Please sign in to comment.