Skip to content

Commit bc92e4e

Browse files
committed
Merge pull request #5596 from RyanCavanaugh/statelessFunctionComponents
Stateless function components in JSX
2 parents 0464138 + 783f65c commit bc92e4e

11 files changed

+18660
-31
lines changed

src/compiler/checker.ts

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,6 @@ namespace ts {
141141
let globalRegExpType: ObjectType;
142142
let globalTemplateStringsArrayType: ObjectType;
143143
let globalESSymbolType: ObjectType;
144-
let jsxElementType: ObjectType;
145-
/** Lazily loaded, use getJsxIntrinsicElementType() */
146-
let jsxIntrinsicElementsType: ObjectType;
147144
let globalIterableType: GenericType;
148145
let globalIteratorType: GenericType;
149146
let globalIterableIteratorType: GenericType;
@@ -208,12 +205,17 @@ namespace ts {
208205
}
209206
};
210207

208+
let jsxElementType: ObjectType;
209+
/** Things we lazy load from the JSX namespace */
210+
const jsxTypes: Map<ObjectType> = {};
211211
const JsxNames = {
212212
JSX: "JSX",
213213
IntrinsicElements: "IntrinsicElements",
214214
ElementClass: "ElementClass",
215215
ElementAttributesPropertyNameContainer: "ElementAttributesProperty",
216-
Element: "Element"
216+
Element: "Element",
217+
IntrinsicAttributes: "IntrinsicAttributes",
218+
IntrinsicClassAttributes: "IntrinsicClassAttributes"
217219
};
218220

219221
const subtypeRelation: Map<RelationComparisonResult> = {};
@@ -7868,12 +7870,11 @@ namespace ts {
78687870
return type;
78697871
}
78707872

7871-
/// Returns the type JSX.IntrinsicElements. May return `unknownType` if that type is not present.
7872-
function getJsxIntrinsicElementsType() {
7873-
if (!jsxIntrinsicElementsType) {
7874-
jsxIntrinsicElementsType = getExportedTypeFromNamespace(JsxNames.JSX, JsxNames.IntrinsicElements) || unknownType;
7873+
function getJsxType(name: string) {
7874+
if (jsxTypes[name] === undefined) {
7875+
return jsxTypes[name] = getExportedTypeFromNamespace(JsxNames.JSX, name) || unknownType;
78757876
}
7876-
return jsxIntrinsicElementsType;
7877+
return jsxTypes[name];
78777878
}
78787879

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

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

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

79627963
const classSymbol = getJsxElementTagSymbol(node);
79637964
if (classSymbol === unknownSymbol) {
@@ -7984,15 +7985,7 @@ namespace ts {
79847985
}
79857986
}
79867987

7987-
const returnType = getUnionType(signatures.map(getReturnTypeOfSignature));
7988-
7989-
// Issue an error if this return type isn't assignable to JSX.ElementClass
7990-
const elemClassType = getJsxGlobalElementClassType();
7991-
if (elemClassType) {
7992-
checkTypeRelatedTo(returnType, elemClassType, assignableRelation, node, Diagnostics.JSX_element_type_0_is_not_a_constructor_function_for_JSX_elements);
7993-
}
7994-
7995-
return returnType;
7988+
return getUnionType(signatures.map(getReturnTypeOfSignature));
79967989
}
79977990

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

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

8041+
// Is this is a stateless function component? See if its single signature is
8042+
// assignable to the JSX Element Type
8043+
const callSignature = getSingleCallSignature(getTypeOfSymbol(sym));
8044+
const callReturnType = callSignature && getReturnTypeOfSignature(callSignature);
8045+
let paramType = callReturnType && (callSignature.parameters.length === 0 ? emptyObjectType : getTypeOfSymbol(callSignature.parameters[0]));
8046+
if (callReturnType && isTypeAssignableTo(callReturnType, jsxElementType) && (paramType.flags & TypeFlags.ObjectType)) {
8047+
// Intersect in JSX.IntrinsicAttributes if it exists
8048+
const intrinsicAttributes = getJsxType(JsxNames.IntrinsicAttributes);
8049+
if (intrinsicAttributes !== unknownType) {
8050+
paramType = intersectTypes(intrinsicAttributes, paramType);
8051+
}
8052+
return paramType;
8053+
}
8054+
8055+
// Issue an error if this return type isn't assignable to JSX.ElementClass
8056+
const elemClassType = getJsxGlobalElementClassType();
8057+
if (elemClassType) {
8058+
checkTypeRelatedTo(elemInstanceType, elemClassType, assignableRelation, node, Diagnostics.JSX_element_type_0_is_not_a_constructor_function_for_JSX_elements);
8059+
}
8060+
8061+
80478062
if (isTypeAny(elemInstanceType)) {
80488063
return links.resolvedJsxType = elemInstanceType;
80498064
}
@@ -8065,14 +8080,36 @@ namespace ts {
80658080
return links.resolvedJsxType = emptyObjectType;
80668081
}
80678082
else if (isTypeAny(attributesType) || (attributesType === unknownType)) {
8083+
// Props is of type 'any' or unknown
80688084
return links.resolvedJsxType = attributesType;
80698085
}
80708086
else if (!(attributesType.flags & TypeFlags.ObjectType)) {
8087+
// Props is not an object type
80718088
error(node.tagName, Diagnostics.JSX_element_attributes_type_0_must_be_an_object_type, typeToString(attributesType));
80728089
return links.resolvedJsxType = anyType;
80738090
}
80748091
else {
8075-
return links.resolvedJsxType = attributesType;
8092+
// Normal case -- add in IntrinsicClassElements<T> and IntrinsicElements
8093+
let apparentAttributesType = attributesType;
8094+
const intrinsicClassAttribs = getJsxType(JsxNames.IntrinsicClassAttributes);
8095+
if (intrinsicClassAttribs !== unknownType) {
8096+
const typeParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(intrinsicClassAttribs.symbol);
8097+
if (typeParams) {
8098+
if (typeParams.length === 1) {
8099+
apparentAttributesType = intersectTypes(createTypeReference(<GenericType>intrinsicClassAttribs, [elemInstanceType]), apparentAttributesType);
8100+
}
8101+
}
8102+
else {
8103+
apparentAttributesType = intersectTypes(attributesType, intrinsicClassAttribs);
8104+
}
8105+
}
8106+
8107+
const intrinsicAttribs = getJsxType(JsxNames.IntrinsicAttributes);
8108+
if (intrinsicAttribs !== unknownType) {
8109+
apparentAttributesType = intersectTypes(intrinsicAttribs, apparentAttributesType);
8110+
}
8111+
8112+
return links.resolvedJsxType = apparentAttributesType;
80768113
}
80778114
}
80788115
}
@@ -8111,7 +8148,7 @@ namespace ts {
81118148

81128149
/// Returns all the properties of the Jsx.IntrinsicElements interface
81138150
function getJsxIntrinsicTagNames(): Symbol[] {
8114-
const intrinsics = getJsxIntrinsicElementsType();
8151+
const intrinsics = getJsxType(JsxNames.IntrinsicElements);
81158152
return intrinsics ? getPropertiesOfType(intrinsics) : emptyArray;
81168153
}
81178154

src/compiler/types.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -439,12 +439,16 @@ namespace ts {
439439

440440
export const enum JsxFlags {
441441
None = 0,
442+
/** An element from a named property of the JSX.IntrinsicElements interface */
442443
IntrinsicNamedElement = 1 << 0,
444+
/** An element inferred from the string index signature of the JSX.IntrinsicElements interface */
443445
IntrinsicIndexedElement = 1 << 1,
444-
ClassElement = 1 << 2,
445-
UnknownElement = 1 << 3,
446+
/** An element backed by a class, class-like, or function value */
447+
ValueElement = 1 << 2,
448+
/** Element resolution failed */
449+
UnknownElement = 1 << 4,
446450

447-
IntrinsicElement = IntrinsicNamedElement | IntrinsicIndexedElement
451+
IntrinsicElement = IntrinsicNamedElement | IntrinsicIndexedElement,
448452
}
449453

450454

src/harness/harness.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,7 @@ namespace Harness {
908908
useCaseSensitiveFileNames?: boolean;
909909
includeBuiltFile?: string;
910910
baselineFile?: string;
911+
libFiles?: string;
911912
}
912913

913914
// Additional options not already in ts.optionDeclarations
@@ -917,6 +918,7 @@ namespace Harness {
917918
{ name: "baselineFile", type: "string" },
918919
{ name: "includeBuiltFile", type: "string" },
919920
{ name: "fileName", type: "string" },
921+
{ name: "libFiles", type: "string" },
920922
{ name: "noErrorTruncation", type: "boolean" }
921923
];
922924

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

997999
// Parse settings
998-
let useCaseSensitiveFileNames = Harness.IO.useCaseSensitiveFileNames();
9991000
if (harnessSettings) {
10001001
setCompilerOptionsFromHarnessSetting(harnessSettings, options);
10011002
}
1002-
if (options.useCaseSensitiveFileNames !== undefined) {
1003-
useCaseSensitiveFileNames = options.useCaseSensitiveFileNames;
1004-
}
10051003

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

10181017
const fileOutputs: GeneratedFile[] = [];
10191018

1019+
// Files from tests\lib that are requested by "@libFiles"
1020+
if (options.libFiles) {
1021+
for (const fileName of options.libFiles.split(",")) {
1022+
const libFileName = "tests/lib/" + fileName;
1023+
programFiles.push({ unitName: libFileName, content: normalizeLineEndings(IO.readFile(libFileName), Harness.IO.newLine()) });
1024+
}
1025+
}
1026+
1027+
10201028
const programFileNames = programFiles.map(file => file.unitName);
10211029

10221030
const compilerHost = createCompilerHost(
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
tests/cases/conformance/jsx/file.tsx(12,9): error TS2324: Property 'name' is missing in type 'IntrinsicAttributes & { name: string; }'.
2+
tests/cases/conformance/jsx/file.tsx(12,16): error TS2339: Property 'naaame' does not exist on type 'IntrinsicAttributes & { name: string; }'.
3+
tests/cases/conformance/jsx/file.tsx(19,15): error TS2322: Type 'number' is not assignable to type 'string'.
4+
tests/cases/conformance/jsx/file.tsx(21,15): error TS2339: Property 'naaaaaaame' does not exist on type 'IntrinsicAttributes & { name?: string; }'.
5+
6+
7+
==== tests/cases/conformance/jsx/file.tsx (4 errors) ====
8+
9+
function Greet(x: {name: string}) {
10+
return <div>Hello, {x}</div>;
11+
}
12+
function Meet({name = 'world'}) {
13+
return <div>Hello, {name}</div>;
14+
}
15+
16+
// OK
17+
let a = <Greet name='world' />;
18+
// Error
19+
let b = <Greet naaame='world' />;
20+
~~~~~~~~~~~~~~~~~~~~~~~~
21+
!!! error TS2324: Property 'name' is missing in type 'IntrinsicAttributes & { name: string; }'.
22+
~~~~~~
23+
!!! error TS2339: Property 'naaame' does not exist on type 'IntrinsicAttributes & { name: string; }'.
24+
25+
// OK
26+
let c = <Meet />;
27+
// OK
28+
let d = <Meet name='me' />;
29+
// Error
30+
let e = <Meet name={42} />;
31+
~~~~~~~~~
32+
!!! error TS2322: Type 'number' is not assignable to type 'string'.
33+
// Error
34+
let f = <Meet naaaaaaame='no' />;
35+
~~~~~~~~~~
36+
!!! error TS2339: Property 'naaaaaaame' does not exist on type 'IntrinsicAttributes & { name?: string; }'.
37+
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//// [file.tsx]
2+
3+
function Greet(x: {name: string}) {
4+
return <div>Hello, {x}</div>;
5+
}
6+
function Meet({name = 'world'}) {
7+
return <div>Hello, {name}</div>;
8+
}
9+
10+
// OK
11+
let a = <Greet name='world' />;
12+
// Error
13+
let b = <Greet naaame='world' />;
14+
15+
// OK
16+
let c = <Meet />;
17+
// OK
18+
let d = <Meet name='me' />;
19+
// Error
20+
let e = <Meet name={42} />;
21+
// Error
22+
let f = <Meet naaaaaaame='no' />;
23+
24+
25+
//// [file.jsx]
26+
function Greet(x) {
27+
return <div>Hello, {x}</div>;
28+
}
29+
function Meet(_a) {
30+
var _b = _a.name, name = _b === void 0 ? 'world' : _b;
31+
return <div>Hello, {name}</div>;
32+
}
33+
// OK
34+
var a = <Greet name='world'/>;
35+
// Error
36+
var b = <Greet naaame='world'/>;
37+
// OK
38+
var c = <Meet />;
39+
// OK
40+
var d = <Meet name='me'/>;
41+
// Error
42+
var e = <Meet name={42}/>;
43+
// Error
44+
var f = <Meet naaaaaaame='no'/>;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
tests/cases/conformance/jsx/file.tsx(2,1): error TS1148: Cannot compile modules unless the '--module' flag is provided.
2+
tests/cases/conformance/jsx/file.tsx(20,16): error TS2339: Property 'ref' does not exist on type 'IntrinsicAttributes & { name?: string; }'.
3+
tests/cases/conformance/jsx/file.tsx(26,42): error TS2339: Property 'subtr' does not exist on type 'string'.
4+
tests/cases/conformance/jsx/file.tsx(28,33): error TS2339: Property 'notARealProperty' does not exist on type 'BigGreeter'.
5+
tests/cases/conformance/jsx/file.tsx(36,26): error TS2339: Property 'propertyNotOnHtmlDivElement' does not exist on type 'HTMLDivElement'.
6+
7+
8+
==== tests/cases/conformance/jsx/file.tsx (5 errors) ====
9+
10+
import React = require('react');
11+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
12+
!!! error TS1148: Cannot compile modules unless the '--module' flag is provided.
13+
14+
function Greet(x: {name?: string}) {
15+
return <div>Hello, {x}</div>;
16+
}
17+
18+
class BigGreeter extends React.Component<{ name?: string }, {}> {
19+
render() {
20+
return <div></div>;
21+
}
22+
greeting: string;
23+
}
24+
25+
// OK
26+
let a = <Greet />;
27+
// OK - always valid to specify 'key'
28+
let b = <Greet key="k" />;
29+
// Error - not allowed to specify 'ref' on SFCs
30+
let c = <Greet ref="myRef" />;
31+
~~~
32+
!!! error TS2339: Property 'ref' does not exist on type 'IntrinsicAttributes & { name?: string; }'.
33+
34+
35+
// OK - ref is valid for classes
36+
let d = <BigGreeter ref={x => x.greeting.substr(10)} />;
37+
// Error ('subtr' not on string)
38+
let e = <BigGreeter ref={x => x.greeting.subtr(10)} />;
39+
~~~~~
40+
!!! error TS2339: Property 'subtr' does not exist on type 'string'.
41+
// Error (ref callback is contextually typed)
42+
let f = <BigGreeter ref={x => x.notARealProperty} />;
43+
~~~~~~~~~~~~~~~~
44+
!!! error TS2339: Property 'notARealProperty' does not exist on type 'BigGreeter'.
45+
46+
// OK - key is always valid
47+
let g = <BigGreeter key={100} />;
48+
49+
// OK - contextually typed intrinsic ref callback parameter
50+
let h = <div ref={x => x.innerText} />;
51+
// Error - property not on ontextually typed intrinsic ref callback parameter
52+
let i = <div ref={x => x.propertyNotOnHtmlDivElement} />;
53+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
54+
!!! error TS2339: Property 'propertyNotOnHtmlDivElement' does not exist on type 'HTMLDivElement'.
55+
56+

0 commit comments

Comments
 (0)