Skip to content

Stateless function components in JSX #5596

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

Merged
85 changes: 61 additions & 24 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,6 @@ namespace ts {
let globalRegExpType: ObjectType;
let globalTemplateStringsArrayType: ObjectType;
let globalESSymbolType: ObjectType;
let jsxElementType: ObjectType;
/** Lazily loaded, use getJsxIntrinsicElementType() */
let jsxIntrinsicElementsType: ObjectType;
let globalIterableType: GenericType;
let globalIteratorType: GenericType;
let globalIterableIteratorType: GenericType;
Expand Down Expand Up @@ -208,12 +205,17 @@ namespace ts {
}
};

let jsxElementType: ObjectType;
/** Things we lazy load from the JSX namespace */
const jsxTypes: Map<ObjectType> = {};
const JsxNames = {
JSX: "JSX",
IntrinsicElements: "IntrinsicElements",
ElementClass: "ElementClass",
ElementAttributesPropertyNameContainer: "ElementAttributesProperty",
Element: "Element"
Element: "Element",
IntrinsicAttributes: "IntrinsicAttributes",
IntrinsicClassAttributes: "IntrinsicClassAttributes"
};

const subtypeRelation: Map<RelationComparisonResult> = {};
Expand Down Expand Up @@ -7868,12 +7870,11 @@ namespace ts {
return type;
}

/// Returns the type JSX.IntrinsicElements. May return `unknownType` if that type is not present.
function getJsxIntrinsicElementsType() {
if (!jsxIntrinsicElementsType) {
jsxIntrinsicElementsType = getExportedTypeFromNamespace(JsxNames.JSX, JsxNames.IntrinsicElements) || unknownType;
function getJsxType(name: string) {
if (jsxTypes[name] === undefined) {
return jsxTypes[name] = getExportedTypeFromNamespace(JsxNames.JSX, name) || unknownType;
}
return jsxIntrinsicElementsType;
return jsxTypes[name];
}

/// Given a JSX opening element or self-closing element, return the symbol of the property that the tag name points to if
Expand All @@ -7896,7 +7897,7 @@ namespace ts {
return links.resolvedSymbol;

function lookupIntrinsicTag(node: JsxOpeningLikeElement | JsxClosingElement): Symbol {
const intrinsicElementsType = getJsxIntrinsicElementsType();
const intrinsicElementsType = getJsxType(JsxNames.IntrinsicElements);
if (intrinsicElementsType !== unknownType) {
// Property case
const intrinsicProp = getPropertyOfType(intrinsicElementsType, (<Identifier>node.tagName).text);
Expand Down Expand Up @@ -7928,7 +7929,7 @@ namespace ts {

// Look up the value in the current scope
if (valueSymbol && valueSymbol !== unknownSymbol) {
links.jsxFlags |= JsxFlags.ClassElement;
links.jsxFlags |= JsxFlags.ValueElement;
if (valueSymbol.flags & SymbolFlags.Alias) {
markAliasSymbolAsReferenced(valueSymbol);
}
Expand Down Expand Up @@ -7957,7 +7958,7 @@ namespace ts {
function getJsxElementInstanceType(node: JsxOpeningLikeElement) {
// There is no such thing as an instance type for a non-class element. This
// line shouldn't be hit.
Debug.assert(!!(getNodeLinks(node).jsxFlags & JsxFlags.ClassElement), "Should not call getJsxElementInstanceType on non-class Element");
Debug.assert(!!(getNodeLinks(node).jsxFlags & JsxFlags.ValueElement), "Should not call getJsxElementInstanceType on non-class Element");

const classSymbol = getJsxElementTagSymbol(node);
if (classSymbol === unknownSymbol) {
Expand All @@ -7984,15 +7985,7 @@ namespace ts {
}
}

const returnType = getUnionType(signatures.map(getReturnTypeOfSignature));

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

return returnType;
return getUnionType(signatures.map(getReturnTypeOfSignature));
}

/// e.g. "props" for React.d.ts,
Expand Down Expand Up @@ -8041,9 +8034,31 @@ namespace ts {
if (!links.resolvedJsxType) {
const sym = getJsxElementTagSymbol(node);

if (links.jsxFlags & JsxFlags.ClassElement) {
if (links.jsxFlags & JsxFlags.ValueElement) {
// Get the element instance type (the result of newing or invoking this tag)
const elemInstanceType = getJsxElementInstanceType(node);

// Is this is a stateless function component? See if its single signature is
// assignable to the JSX Element Type
const callSignature = getSingleCallSignature(getTypeOfSymbol(sym));
const callReturnType = callSignature && getReturnTypeOfSignature(callSignature);
let paramType = callReturnType && (callSignature.parameters.length === 0 ? emptyObjectType : getTypeOfSymbol(callSignature.parameters[0]));
if (callReturnType && isTypeAssignableTo(callReturnType, jsxElementType) && (paramType.flags & TypeFlags.ObjectType)) {
// 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
const elemClassType = getJsxGlobalElementClassType();
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;
}
Expand All @@ -8065,14 +8080,36 @@ namespace ts {
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.ObjectType)) {
// Props is not an object type
error(node.tagName, Diagnostics.JSX_element_attributes_type_0_must_be_an_object_type, typeToString(attributesType));
return links.resolvedJsxType = anyType;
}
else {
return links.resolvedJsxType = attributesType;
// 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;
}
}
}
Expand Down Expand Up @@ -8111,7 +8148,7 @@ namespace ts {

/// Returns all the properties of the Jsx.IntrinsicElements interface
function getJsxIntrinsicTagNames(): Symbol[] {
const intrinsics = getJsxIntrinsicElementsType();
const intrinsics = getJsxType(JsxNames.IntrinsicElements);
return intrinsics ? getPropertiesOfType(intrinsics) : emptyArray;
}

Expand Down
10 changes: 7 additions & 3 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,12 +439,16 @@ namespace ts {

export const enum JsxFlags {
None = 0,
/** An element from a named property of the JSX.IntrinsicElements interface */
IntrinsicNamedElement = 1 << 0,
/** An element inferred from the string index signature of the JSX.IntrinsicElements interface */
IntrinsicIndexedElement = 1 << 1,
ClassElement = 1 << 2,
UnknownElement = 1 << 3,
/** An element backed by a class, class-like, or function value */
ValueElement = 1 << 2,
/** Element resolution failed */
UnknownElement = 1 << 4,

IntrinsicElement = IntrinsicNamedElement | IntrinsicIndexedElement
IntrinsicElement = IntrinsicNamedElement | IntrinsicIndexedElement,
}


Expand Down
16 changes: 12 additions & 4 deletions src/harness/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,7 @@ namespace Harness {
useCaseSensitiveFileNames?: boolean;
includeBuiltFile?: string;
baselineFile?: string;
libFiles?: string;
}

// Additional options not already in ts.optionDeclarations
Expand All @@ -917,6 +918,7 @@ namespace Harness {
{ name: "baselineFile", type: "string" },
{ name: "includeBuiltFile", type: "string" },
{ name: "fileName", type: "string" },
{ name: "libFiles", type: "string" },
{ name: "noErrorTruncation", type: "boolean" }
];

Expand Down Expand Up @@ -995,14 +997,11 @@ namespace Harness {
currentDirectory = currentDirectory || Harness.IO.getCurrentDirectory();

// Parse settings
let useCaseSensitiveFileNames = Harness.IO.useCaseSensitiveFileNames();
if (harnessSettings) {
setCompilerOptionsFromHarnessSetting(harnessSettings, options);
}
if (options.useCaseSensitiveFileNames !== undefined) {
useCaseSensitiveFileNames = options.useCaseSensitiveFileNames;
}

const useCaseSensitiveFileNames = options.useCaseSensitiveFileNames !== undefined ? options.useCaseSensitiveFileNames : Harness.IO.useCaseSensitiveFileNames();
const programFiles: TestFile[] = inputFiles.slice();
// Files from built\local that are requested by test "@includeBuiltFiles" to be in the context.
// Treat them as library files, so include them in build, but not in baselines.
Expand All @@ -1017,6 +1016,15 @@ namespace Harness {

const fileOutputs: GeneratedFile[] = [];

// Files from tests\lib that are requested by "@libFiles"
if (options.libFiles) {
for (const fileName of options.libFiles.split(",")) {
const libFileName = "tests/lib/" + fileName;
programFiles.push({ unitName: libFileName, content: normalizeLineEndings(IO.readFile(libFileName), Harness.IO.newLine()) });
}
}


const programFileNames = programFiles.map(file => file.unitName);

const compilerHost = createCompilerHost(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
tests/cases/conformance/jsx/file.tsx(12,9): error TS2324: Property 'name' is missing in type 'IntrinsicAttributes & { name: string; }'.
tests/cases/conformance/jsx/file.tsx(12,16): error TS2339: Property 'naaame' does not exist on type 'IntrinsicAttributes & { name: string; }'.
tests/cases/conformance/jsx/file.tsx(19,15): error TS2322: Type 'number' is not assignable to type 'string'.
tests/cases/conformance/jsx/file.tsx(21,15): error TS2339: Property 'naaaaaaame' does not exist on type 'IntrinsicAttributes & { name?: string; }'.


==== tests/cases/conformance/jsx/file.tsx (4 errors) ====

function Greet(x: {name: string}) {
return <div>Hello, {x}</div>;
}
function Meet({name = 'world'}) {
return <div>Hello, {name}</div>;
}

// OK
let a = <Greet name='world' />;
// Error
let b = <Greet naaame='world' />;
~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2324: Property 'name' is missing in type 'IntrinsicAttributes & { name: string; }'.
~~~~~~
!!! error TS2339: Property 'naaame' does not exist on type 'IntrinsicAttributes & { name: string; }'.

// OK
let c = <Meet />;
// OK
let d = <Meet name='me' />;
// Error
let e = <Meet name={42} />;
~~~~~~~~~
!!! error TS2322: Type 'number' is not assignable to type 'string'.
// Error
let f = <Meet naaaaaaame='no' />;
~~~~~~~~~~
!!! error TS2339: Property 'naaaaaaame' does not exist on type 'IntrinsicAttributes & { name?: string; }'.

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

function Greet(x: {name: string}) {
return <div>Hello, {x}</div>;
}
function Meet({name = 'world'}) {
return <div>Hello, {name}</div>;
}

// OK
let a = <Greet name='world' />;
// Error
let b = <Greet naaame='world' />;

// OK
let c = <Meet />;
// OK
let d = <Meet name='me' />;
// Error
let e = <Meet name={42} />;
// Error
let f = <Meet naaaaaaame='no' />;


//// [file.jsx]
function Greet(x) {
return <div>Hello, {x}</div>;
}
function Meet(_a) {
var _b = _a.name, name = _b === void 0 ? 'world' : _b;
return <div>Hello, {name}</div>;
}
// OK
var a = <Greet name='world'/>;
// Error
var b = <Greet naaame='world'/>;
// OK
var c = <Meet />;
// OK
var d = <Meet name='me'/>;
// Error
var e = <Meet name={42}/>;
// Error
var f = <Meet naaaaaaame='no'/>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
tests/cases/conformance/jsx/file.tsx(2,1): error TS1148: Cannot compile modules unless the '--module' flag is provided.
tests/cases/conformance/jsx/file.tsx(20,16): error TS2339: Property 'ref' does not exist on type 'IntrinsicAttributes & { name?: string; }'.
tests/cases/conformance/jsx/file.tsx(26,42): error TS2339: Property 'subtr' does not exist on type 'string'.
tests/cases/conformance/jsx/file.tsx(28,33): error TS2339: Property 'notARealProperty' does not exist on type 'BigGreeter'.
tests/cases/conformance/jsx/file.tsx(36,26): error TS2339: Property 'propertyNotOnHtmlDivElement' does not exist on type 'HTMLDivElement'.


==== tests/cases/conformance/jsx/file.tsx (5 errors) ====

import React = require('react');
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS1148: Cannot compile modules unless the '--module' flag is provided.

function Greet(x: {name?: string}) {
return <div>Hello, {x}</div>;
}

class BigGreeter extends React.Component<{ name?: string }, {}> {
render() {
return <div></div>;
}
greeting: string;
}

// OK
let a = <Greet />;
// OK - always valid to specify 'key'
let b = <Greet key="k" />;
// Error - not allowed to specify 'ref' on SFCs
let c = <Greet ref="myRef" />;
~~~
!!! error TS2339: Property 'ref' does not exist on type 'IntrinsicAttributes & { name?: string; }'.


// OK - ref is valid for classes
let d = <BigGreeter ref={x => x.greeting.substr(10)} />;
// Error ('subtr' not on string)
let e = <BigGreeter ref={x => x.greeting.subtr(10)} />;
~~~~~
!!! error TS2339: Property 'subtr' does not exist on type 'string'.
// Error (ref callback is contextually typed)
let f = <BigGreeter ref={x => x.notARealProperty} />;
~~~~~~~~~~~~~~~~
!!! error TS2339: Property 'notARealProperty' does not exist on type 'BigGreeter'.

// OK - key is always valid
let g = <BigGreeter key={100} />;

// OK - contextually typed intrinsic ref callback parameter
let h = <div ref={x => x.innerText} />;
// Error - property not on ontextually typed intrinsic ref callback parameter
let i = <div ref={x => x.propertyNotOnHtmlDivElement} />;
~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2339: Property 'propertyNotOnHtmlDivElement' does not exist on type 'HTMLDivElement'.


Loading