Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #8657: Handles union typed React component. #8674

Merged
merged 7 commits into from
May 19, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 104 additions & 90 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9504,20 +9504,18 @@ namespace ts {
* element is not a class element, or the class element type cannot be determined, returns 'undefined'.
* For example, in the element <MyClass>, the element instance type is `MyClass` (not `typeof MyClass`).
*/
function getJsxElementInstanceType(node: JsxOpeningLikeElement) {
const valueType = checkExpression(node.tagName);

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 constructors
// 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));
Expand Down Expand Up @@ -9565,6 +9563,103 @@ namespace ts {
}
}

/**
* Given React element instance type and the class type, resolve the Jsx type
* Pass elemType to handle individual type in the union typed element type.
*/
function getResolvedJsxType(node: JsxOpeningLikeElement, elemType?: Type, elemClassType?: Type): Type {
if (!elemType) {
elemType = checkExpression(node.tagName);
}
if (elemType.flags & TypeFlags.Union) {
const types = (<UnionOrIntersectionType> elemType).types;
return getUnionType(types.map(type => {
return getResolvedJsxType(node, type, elemClassType);
}));
}

// Get the element instance type (the result of newing or invoking this tag)
const elemInstanceType = getJsxElementInstanceType(node, elemType);

if (!elemClassType || !isTypeAssignableTo(elemInstanceType, elemClassType)) {
// Is this is a stateless function component? See if its single signature's return type is
// assignable to the JSX Element Type
if (jsxElementType) {
const callSignatures = elemType && getSignaturesOfType(elemType, SignatureKind.Call);
const callSignature = callSignatures && callSignatures.length > 0 && callSignatures[0];
const callReturnType = callSignature && getReturnTypeOfSignature(callSignature);
let paramType = callReturnType && (callSignature.parameters.length === 0 ? emptyObjectType : getTypeOfSymbol(callSignature.parameters[0]));
if (callReturnType && isTypeAssignableTo(callReturnType, jsxElementType)) {
// Intersect in JSX.IntrinsicAttributes if it exists
const intrinsicAttributes = getJsxType(JsxNames.IntrinsicAttributes);
if (intrinsicAttributes !== unknownType) {
paramType = intersectTypes(intrinsicAttributes, paramType);
}
return paramType;
}
}
}

// Issue an error if this return type isn't assignable to JSX.ElementClass
if (elemClassType) {
checkTypeRelatedTo(elemInstanceType, elemClassType, assignableRelation, node, Diagnostics.JSX_element_type_0_is_not_a_constructor_function_for_JSX_elements);
}

if (isTypeAny(elemInstanceType)) {
return elemInstanceType;
}

const propsName = getJsxElementPropertiesName();
if (propsName === undefined) {
// There is no type ElementAttributesProperty, return 'any'
return anyType;
}
else if (propsName === "") {
// If there is no e.g. 'props' member in ElementAttributesProperty, use the element class type instead
return elemInstanceType;
}
else {
const attributesType = getTypeOfPropertyOfType(elemInstanceType, propsName);

if (!attributesType) {
// There is no property named 'props' on this instance type
return emptyObjectType;
}
else if (isTypeAny(attributesType) || (attributesType === unknownType)) {
// Props is of type 'any' or unknown
return attributesType;
}
else if (attributesType.flags & TypeFlags.Union) {
// Props cannot be a union type
error(node.tagName, Diagnostics.JSX_element_attributes_type_0_may_not_be_a_union_type, typeToString(attributesType));
return anyType;
}
else {
// Normal case -- add in IntrinsicClassElements<T> and IntrinsicElements
let apparentAttributesType = attributesType;
const intrinsicClassAttribs = getJsxType(JsxNames.IntrinsicClassAttributes);
if (intrinsicClassAttribs !== unknownType) {
const typeParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(intrinsicClassAttribs.symbol);
if (typeParams) {
if (typeParams.length === 1) {
apparentAttributesType = intersectTypes(createTypeReference(<GenericType>intrinsicClassAttribs, [elemInstanceType]), apparentAttributesType);
}
}
else {
apparentAttributesType = intersectTypes(attributesType, intrinsicClassAttribs);
}
}

const intrinsicAttribs = getJsxType(JsxNames.IntrinsicAttributes);
if (intrinsicAttribs !== unknownType) {
apparentAttributesType = intersectTypes(intrinsicAttribs, apparentAttributesType);
}

return apparentAttributesType;
}
}
}

/**
* Given an opening/self-closing element, get the 'element attributes type', i.e. the type that tells
* us which attributes are valid on a given element.
Expand All @@ -9580,96 +9675,15 @@ namespace ts {
else if (links.jsxFlags & JsxFlags.IntrinsicIndexedElement) {
return links.resolvedJsxType = getIndexInfoOfSymbol(symbol, IndexKind.String).type;
}
else {
return links.resolvedJsxType = unknownType;
}
}
else {
// Get the element instance type (the result of newing or invoking this tag)
const elemInstanceType = getJsxElementInstanceType(node);

const elemClassType = getJsxGlobalElementClassType();

if (!elemClassType || !isTypeAssignableTo(elemInstanceType, elemClassType)) {
// Is this is a stateless function component? See if its single signature's return type is
// assignable to the JSX Element Type
if (jsxElementType) {
const elemType = checkExpression(node.tagName);
const callSignatures = elemType && getSignaturesOfType(elemType, SignatureKind.Call);
const callSignature = callSignatures && callSignatures.length > 0 && callSignatures[0];
const callReturnType = callSignature && getReturnTypeOfSignature(callSignature);
let paramType = callReturnType && (callSignature.parameters.length === 0 ? emptyObjectType : getTypeOfSymbol(callSignature.parameters[0]));
if (callReturnType && isTypeAssignableTo(callReturnType, jsxElementType)) {
// Intersect in JSX.IntrinsicAttributes if it exists
const intrinsicAttributes = getJsxType(JsxNames.IntrinsicAttributes);
if (intrinsicAttributes !== unknownType) {
paramType = intersectTypes(intrinsicAttributes, paramType);
}
return links.resolvedJsxType = paramType;
}
}
}

// Issue an error if this return type isn't assignable to JSX.ElementClass
if (elemClassType) {
checkTypeRelatedTo(elemInstanceType, elemClassType, assignableRelation, node, Diagnostics.JSX_element_type_0_is_not_a_constructor_function_for_JSX_elements);
}

if (isTypeAny(elemInstanceType)) {
return links.resolvedJsxType = elemInstanceType;
}

const propsName = getJsxElementPropertiesName();
if (propsName === undefined) {
// There is no type ElementAttributesProperty, return 'any'
return links.resolvedJsxType = anyType;
}
else if (propsName === "") {
// If there is no e.g. 'props' member in ElementAttributesProperty, use the element class type instead
return links.resolvedJsxType = elemInstanceType;
}
else {
const attributesType = getTypeOfPropertyOfType(elemInstanceType, propsName);

if (!attributesType) {
// There is no property named 'props' on this instance type
return links.resolvedJsxType = emptyObjectType;
}
else if (isTypeAny(attributesType) || (attributesType === unknownType)) {
// Props is of type 'any' or unknown
return links.resolvedJsxType = attributesType;
}
else if (attributesType.flags & TypeFlags.Union) {
// Props cannot be a union type
error(node.tagName, Diagnostics.JSX_element_attributes_type_0_may_not_be_a_union_type, typeToString(attributesType));
return links.resolvedJsxType = anyType;
}
else {
// Normal case -- add in IntrinsicClassElements<T> and IntrinsicElements
let apparentAttributesType = attributesType;
const intrinsicClassAttribs = getJsxType(JsxNames.IntrinsicClassAttributes);
if (intrinsicClassAttribs !== unknownType) {
const typeParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(intrinsicClassAttribs.symbol);
if (typeParams) {
if (typeParams.length === 1) {
apparentAttributesType = intersectTypes(createTypeReference(<GenericType>intrinsicClassAttribs, [elemInstanceType]), apparentAttributesType);
}
}
else {
apparentAttributesType = intersectTypes(attributesType, intrinsicClassAttribs);
}
}

const intrinsicAttribs = getJsxType(JsxNames.IntrinsicAttributes);
if (intrinsicAttribs !== unknownType) {
apparentAttributesType = intersectTypes(intrinsicAttribs, apparentAttributesType);
}

return links.resolvedJsxType = apparentAttributesType;
}
}
return links.resolvedJsxType = getResolvedJsxType(node, undefined, elemClassType);
}

return links.resolvedJsxType = unknownType;
}

return links.resolvedJsxType;
}

Expand Down
56 changes: 56 additions & 0 deletions tests/baselines/reference/tsxUnionTypeComponent1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//// [file.tsx]

import React = require('react');

interface ComponentProps {
AnyComponent: React.StatelessComponent<any> | React.ComponentClass<any>;
}

class MyComponent extends React.Component<ComponentProps, {}> {
render() {
const { AnyComponent } = this.props;
return (<AnyComponent />);
}
}

// Stateless Component As Props
<MyComponent AnyComponent={() => <button>test</button>}/>

// Component Class as Props
class MyButtonComponent extends React.Component<{},{}> {
}

<MyComponent AnyComponent={MyButtonComponent} />



//// [file.js]
"use strict";
var __extends = (this && this.__extends) || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var React = require('react');
var MyComponent = (function (_super) {
__extends(MyComponent, _super);
function MyComponent() {
_super.apply(this, arguments);
}
MyComponent.prototype.render = function () {
var AnyComponent = this.props.AnyComponent;
return (React.createElement(AnyComponent, null));
};
return MyComponent;
}(React.Component));
// Stateless Component As Props
React.createElement(MyComponent, {AnyComponent: function () { return React.createElement("button", null, "test"); }});
// Component Class as Props
var MyButtonComponent = (function (_super) {
__extends(MyButtonComponent, _super);
function MyButtonComponent() {
_super.apply(this, arguments);
}
return MyButtonComponent;
}(React.Component));
React.createElement(MyComponent, {AnyComponent: MyButtonComponent});
58 changes: 58 additions & 0 deletions tests/baselines/reference/tsxUnionTypeComponent1.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
=== tests/cases/conformance/jsx/file.tsx ===

import React = require('react');
>React : Symbol(React, Decl(file.tsx, 0, 0))

interface ComponentProps {
>ComponentProps : Symbol(ComponentProps, Decl(file.tsx, 1, 32))

AnyComponent: React.StatelessComponent<any> | React.ComponentClass<any>;
>AnyComponent : Symbol(ComponentProps.AnyComponent, Decl(file.tsx, 3, 26))
>React : Symbol(React, Decl(file.tsx, 0, 0))
>StatelessComponent : Symbol(React.StatelessComponent, Decl(react.d.ts, 139, 5))
>React : Symbol(React, Decl(file.tsx, 0, 0))
>ComponentClass : Symbol(React.ComponentClass, Decl(react.d.ts, 150, 5))
}

class MyComponent extends React.Component<ComponentProps, {}> {
>MyComponent : Symbol(MyComponent, Decl(file.tsx, 5, 1))
>React.Component : Symbol(React.Component, Decl(react.d.ts, 114, 55))
>React : Symbol(React, Decl(file.tsx, 0, 0))
>Component : Symbol(React.Component, Decl(react.d.ts, 114, 55))
>ComponentProps : Symbol(ComponentProps, Decl(file.tsx, 1, 32))

render() {
>render : Symbol(MyComponent.render, Decl(file.tsx, 7, 63))

const { AnyComponent } = this.props;
>AnyComponent : Symbol(AnyComponent, Decl(file.tsx, 9, 15))
>this.props : Symbol(React.Component.props, Decl(react.d.ts, 122, 30))
>this : Symbol(MyComponent, Decl(file.tsx, 5, 1))
>props : Symbol(React.Component.props, Decl(react.d.ts, 122, 30))

return (<AnyComponent />);
>AnyComponent : Symbol(AnyComponent, Decl(file.tsx, 9, 15))
}
}

// Stateless Component As Props
<MyComponent AnyComponent={() => <button>test</button>}/>
>MyComponent : Symbol(MyComponent, Decl(file.tsx, 5, 1))
>AnyComponent : Symbol(ComponentProps.AnyComponent, Decl(file.tsx, 3, 26))
>button : Symbol(JSX.IntrinsicElements.button, Decl(react.d.ts, 913, 43))
>button : Symbol(JSX.IntrinsicElements.button, Decl(react.d.ts, 913, 43))

// Component Class as Props
class MyButtonComponent extends React.Component<{},{}> {
>MyButtonComponent : Symbol(MyButtonComponent, Decl(file.tsx, 15, 57))
>React.Component : Symbol(React.Component, Decl(react.d.ts, 114, 55))
>React : Symbol(React, Decl(file.tsx, 0, 0))
>Component : Symbol(React.Component, Decl(react.d.ts, 114, 55))
}

<MyComponent AnyComponent={MyButtonComponent} />
>MyComponent : Symbol(MyComponent, Decl(file.tsx, 5, 1))
>AnyComponent : Symbol(ComponentProps.AnyComponent, Decl(file.tsx, 3, 26))
>MyButtonComponent : Symbol(MyButtonComponent, Decl(file.tsx, 15, 57))


Loading