-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
RFC: Consult new JSX.ElementType for valid JSX element types #51328
Changes from all commits
26e25ae
71a0843
ca648a8
8bda707
8a3307e
511180b
c69d4de
4e39127
4a04633
4b351fd
a4c8220
315e0b2
af48970
9adca32
68a7335
320f52f
79c22f9
14bd3fd
897c643
b7a552e
d02bc61
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -30328,7 +30328,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |||||||||||
/** | ||||||||||||
* Returns true iff React would emit this tag name as a string rather than an identifier or qualified name | ||||||||||||
*/ | ||||||||||||
function isJsxIntrinsicIdentifier(tagName: JsxTagNameExpression): boolean { | ||||||||||||
function isJsxIntrinsicIdentifier(tagName: JsxTagNameExpression): tagName is Identifier { | ||||||||||||
return tagName.kind === SyntaxKind.Identifier && isIntrinsicJsxName(tagName.escapedText); | ||||||||||||
} | ||||||||||||
|
||||||||||||
|
@@ -30793,6 +30793,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
function getJsxElementTypeTypeAt(location: Node): Type | undefined { | ||||||||||||
const type = getJsxType(JsxNames.ElementType, location); | ||||||||||||
if (isErrorType(type)) return undefined; | ||||||||||||
return type; | ||||||||||||
} | ||||||||||||
|
||||||||||||
/** | ||||||||||||
* Returns all the properties of the Jsx.IntrinsicElements interface | ||||||||||||
*/ | ||||||||||||
|
@@ -30861,7 +30867,21 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |||||||||||
const jsxOpeningLikeNode = node ; | ||||||||||||
const sig = getResolvedSignature(jsxOpeningLikeNode); | ||||||||||||
checkDeprecatedSignature(sig, node); | ||||||||||||
checkJsxReturnAssignableToAppropriateBound(getJsxReferenceKind(jsxOpeningLikeNode), getReturnTypeOfSignature(sig), jsxOpeningLikeNode); | ||||||||||||
|
||||||||||||
DanielRosenwasser marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
const elementTypeConstraint = getJsxElementTypeTypeAt(jsxOpeningLikeNode); | ||||||||||||
if (elementTypeConstraint !== undefined) { | ||||||||||||
const tagName = jsxOpeningLikeNode.tagName; | ||||||||||||
const tagType = isJsxIntrinsicIdentifier(tagName) | ||||||||||||
? getStringLiteralType(unescapeLeadingUnderscores(tagName.escapedText)) | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI I assume you meant
Suggested change
though? |
||||||||||||
: checkExpression(tagName); | ||||||||||||
checkTypeRelatedTo(tagType, elementTypeConstraint, assignableRelation, tagName, Diagnostics.Its_type_0_is_not_a_valid_JSX_element_type, () => { | ||||||||||||
const componentName = getTextOfNode(tagName); | ||||||||||||
return chainDiagnosticMessages(/*details*/ undefined, Diagnostics._0_cannot_be_used_as_a_JSX_component, componentName); | ||||||||||||
}); | ||||||||||||
} | ||||||||||||
else { | ||||||||||||
checkJsxReturnAssignableToAppropriateBound(getJsxReferenceKind(jsxOpeningLikeNode), getReturnTypeOfSignature(sig), jsxOpeningLikeNode); | ||||||||||||
} | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
|
@@ -48800,6 +48820,7 @@ namespace JsxNames { | |||||||||||
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 ElementType = "ElementType" as __String; | ||||||||||||
export const IntrinsicAttributes = "IntrinsicAttributes" as __String; | ||||||||||||
export const IntrinsicClassAttributes = "IntrinsicClassAttributes" as __String; | ||||||||||||
export const LibraryManagedAttributes = "LibraryManagedAttributes" as __String; | ||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
tests/cases/compiler/jsxElementType.tsx(34,2): error TS2741: Property 'title' is missing in type '{}' but required in type '{ title: string; }'. | ||
tests/cases/compiler/jsxElementType.tsx(36,16): error TS2322: Type '{ excessProp: true; }' is not assignable to type 'IntrinsicAttributes & { title: string; }'. | ||
Property 'excessProp' does not exist on type 'IntrinsicAttributes & { title: string; }'. | ||
tests/cases/compiler/jsxElementType.tsx(40,2): error TS2741: Property 'title' is missing in type '{}' but required in type '{ title: string; }'. | ||
tests/cases/compiler/jsxElementType.tsx(42,15): error TS2322: Type '{ excessProp: true; }' is not assignable to type 'IntrinsicAttributes & { title: string; }'. | ||
Property 'excessProp' does not exist on type 'IntrinsicAttributes & { title: string; }'. | ||
tests/cases/compiler/jsxElementType.tsx(46,2): error TS2741: Property 'title' is missing in type '{}' but required in type '{ title: string; }'. | ||
tests/cases/compiler/jsxElementType.tsx(48,15): error TS2322: Type '{ excessProp: true; }' is not assignable to type 'IntrinsicAttributes & { title: string; }'. | ||
Property 'excessProp' does not exist on type 'IntrinsicAttributes & { title: string; }'. | ||
tests/cases/compiler/jsxElementType.tsx(52,2): error TS2741: Property 'title' is missing in type '{}' but required in type '{ title: string; }'. | ||
tests/cases/compiler/jsxElementType.tsx(54,14): error TS2322: Type '{ excessProp: true; }' is not assignable to type 'IntrinsicAttributes & { title: string; }'. | ||
Property 'excessProp' does not exist on type 'IntrinsicAttributes & { title: string; }'. | ||
tests/cases/compiler/jsxElementType.tsx(59,2): error TS2741: Property 'title' is missing in type '{}' but required in type '{ title: string; }'. | ||
tests/cases/compiler/jsxElementType.tsx(61,16): error TS2322: Type '{ excessProp: true; }' is not assignable to type 'IntrinsicAttributes & { title: string; }'. | ||
Property 'excessProp' does not exist on type 'IntrinsicAttributes & { title: string; }'. | ||
tests/cases/compiler/jsxElementType.tsx(70,2): error TS2769: No overload matches this call. | ||
Overload 1 of 2, '(props: Readonly<{ title: string; }>): RenderStringClass', gave the following error. | ||
Property 'title' is missing in type '{}' but required in type 'Readonly<{ title: string; }>'. | ||
Overload 2 of 2, '(props: { title: string; }, context?: any): RenderStringClass', gave the following error. | ||
Property 'title' is missing in type '{}' but required in type 'Readonly<{ title: string; }>'. | ||
tests/cases/compiler/jsxElementType.tsx(72,20): error TS2769: No overload matches this call. | ||
Overload 1 of 2, '(props: Readonly<{ title: string; }>): RenderStringClass', gave the following error. | ||
Type '{ excessProp: true; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<RenderStringClass> & Readonly<{ children?: ReactNode; }> & Readonly<{ title: string; }>'. | ||
Property 'excessProp' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<RenderStringClass> & Readonly<{ children?: ReactNode; }> & Readonly<{ title: string; }>'. | ||
Overload 2 of 2, '(props: { title: string; }, context?: any): RenderStringClass', gave the following error. | ||
Type '{ excessProp: true; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<RenderStringClass> & Readonly<{ children?: ReactNode; }> & Readonly<{ title: string; }>'. | ||
Property 'excessProp' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<RenderStringClass> & Readonly<{ children?: ReactNode; }> & Readonly<{ title: string; }>'. | ||
tests/cases/compiler/jsxElementType.tsx(78,1): error TS2339: Property 'boop' does not exist on type 'JSX.IntrinsicElements'. | ||
tests/cases/compiler/jsxElementType.tsx(79,1): error TS2339: Property 'my-undeclared-custom-element' does not exist on type 'JSX.IntrinsicElements'. | ||
tests/cases/compiler/jsxElementType.tsx(91,2): error TS2786: 'ReactNativeFlatList' cannot be used as a JSX component. | ||
Its type '(props: {}, ref: ForwardedRef<typeof ReactNativeFlatList>) => null' is not a valid JSX element type. | ||
Type '(props: {}, ref: ForwardedRef<typeof ReactNativeFlatList>) => null' is not assignable to type '(props: any) => React18ReactNode'. | ||
Target signature provides too few arguments. Expected 2 or more, but got 1. | ||
tests/cases/compiler/jsxElementType.tsx(95,11): error TS2322: Type '{}' is not assignable to type 'LibraryManagedAttributes<T, {}>'. | ||
tests/cases/compiler/jsxElementType.tsx(98,2): error TS2304: Cannot find name 'Unresolved'. | ||
tests/cases/compiler/jsxElementType.tsx(99,2): error TS2304: Cannot find name 'Unresolved'. | ||
|
||
|
||
==== tests/cases/compiler/jsxElementType.tsx (18 errors) ==== | ||
/// <reference path="/.lib/react16.d.ts" /> | ||
import * as React from "react"; | ||
|
||
type React18ReactFragment = ReadonlyArray<React18ReactNode>; | ||
type React18ReactNode = | ||
| React.ReactElement<any> | ||
| string | ||
| number | ||
| React18ReactFragment | ||
| React.ReactPortal | ||
| boolean | ||
| null | ||
| undefined | ||
| Promise<React18ReactNode>; | ||
|
||
// // React.JSXElementConstructor but it now can return React nodes from function components. | ||
type NewReactJSXElementConstructor<P> = | ||
| ((props: P) => React18ReactNode) | ||
| (new (props: P) => React.Component<P, any>); | ||
|
||
declare global { | ||
namespace JSX { | ||
type ElementType = string | NewReactJSXElementConstructor<any>; | ||
interface IntrinsicElements { | ||
['my-custom-element']: React.DOMAttributes<unknown>; | ||
} | ||
} | ||
} | ||
|
||
let Component: NewReactJSXElementConstructor<{ title: string }>; | ||
|
||
const RenderElement = ({ title }: { title: string }) => <div>{title}</div>; | ||
Component = RenderElement; | ||
<RenderElement />; | ||
~~~~~~~~~~~~~ | ||
!!! error TS2741: Property 'title' is missing in type '{}' but required in type '{ title: string; }'. | ||
!!! related TS2728 tests/cases/compiler/jsxElementType.tsx:32:37: 'title' is declared here. | ||
<RenderElement title="react" />; | ||
<RenderElement excessProp />; | ||
~~~~~~~~~~ | ||
!!! error TS2322: Type '{ excessProp: true; }' is not assignable to type 'IntrinsicAttributes & { title: string; }'. | ||
!!! error TS2322: Property 'excessProp' does not exist on type 'IntrinsicAttributes & { title: string; }'. | ||
|
||
const RenderString = ({ title }: { title: string }) => title; | ||
Component = RenderString; | ||
<RenderString />; | ||
~~~~~~~~~~~~ | ||
!!! error TS2741: Property 'title' is missing in type '{}' but required in type '{ title: string; }'. | ||
!!! related TS2728 tests/cases/compiler/jsxElementType.tsx:38:36: 'title' is declared here. | ||
<RenderString title="react" />; | ||
<RenderString excessProp />; | ||
~~~~~~~~~~ | ||
!!! error TS2322: Type '{ excessProp: true; }' is not assignable to type 'IntrinsicAttributes & { title: string; }'. | ||
!!! error TS2322: Property 'excessProp' does not exist on type 'IntrinsicAttributes & { title: string; }'. | ||
|
||
const RenderNumber = ({ title }: { title: string }) => title.length; | ||
Component = RenderNumber; | ||
<RenderNumber />; | ||
~~~~~~~~~~~~ | ||
!!! error TS2741: Property 'title' is missing in type '{}' but required in type '{ title: string; }'. | ||
!!! related TS2728 tests/cases/compiler/jsxElementType.tsx:44:36: 'title' is declared here. | ||
<RenderNumber title="react" />; | ||
<RenderNumber excessProp />; | ||
~~~~~~~~~~ | ||
!!! error TS2322: Type '{ excessProp: true; }' is not assignable to type 'IntrinsicAttributes & { title: string; }'. | ||
!!! error TS2322: Property 'excessProp' does not exist on type 'IntrinsicAttributes & { title: string; }'. | ||
|
||
const RenderArray = ({ title }: { title: string }) => [title]; | ||
Component = RenderArray; | ||
<RenderArray />; | ||
~~~~~~~~~~~ | ||
!!! error TS2741: Property 'title' is missing in type '{}' but required in type '{ title: string; }'. | ||
!!! related TS2728 tests/cases/compiler/jsxElementType.tsx:50:35: 'title' is declared here. | ||
<RenderArray title="react" />; | ||
<RenderArray excessProp />; | ||
~~~~~~~~~~ | ||
!!! error TS2322: Type '{ excessProp: true; }' is not assignable to type 'IntrinsicAttributes & { title: string; }'. | ||
!!! error TS2322: Property 'excessProp' does not exist on type 'IntrinsicAttributes & { title: string; }'. | ||
|
||
// React Server Component | ||
const RenderPromise = async ({ title }: { title: string }) => "react"; | ||
Component = RenderPromise; | ||
<RenderPromise />; | ||
~~~~~~~~~~~~~ | ||
!!! error TS2741: Property 'title' is missing in type '{}' but required in type '{ title: string; }'. | ||
!!! related TS2728 tests/cases/compiler/jsxElementType.tsx:57:43: 'title' is declared here. | ||
<RenderPromise title="react" />; | ||
<RenderPromise excessProp />; | ||
~~~~~~~~~~ | ||
!!! error TS2322: Type '{ excessProp: true; }' is not assignable to type 'IntrinsicAttributes & { title: string; }'. | ||
!!! error TS2322: Property 'excessProp' does not exist on type 'IntrinsicAttributes & { title: string; }'. | ||
|
||
// Class components still work | ||
class RenderStringClass extends React.Component<{ title: string }> { | ||
render() { | ||
return this.props.title; | ||
} | ||
} | ||
Component = RenderStringClass; | ||
<RenderStringClass />; | ||
~~~~~~~~~~~~~~~~~ | ||
!!! error TS2769: No overload matches this call. | ||
!!! error TS2769: Overload 1 of 2, '(props: Readonly<{ title: string; }>): RenderStringClass', gave the following error. | ||
!!! error TS2769: Property 'title' is missing in type '{}' but required in type 'Readonly<{ title: string; }>'. | ||
!!! error TS2769: Overload 2 of 2, '(props: { title: string; }, context?: any): RenderStringClass', gave the following error. | ||
!!! error TS2769: Property 'title' is missing in type '{}' but required in type 'Readonly<{ title: string; }>'. | ||
!!! related TS2728 tests/cases/compiler/jsxElementType.tsx:64:51: 'title' is declared here. | ||
!!! related TS2728 tests/cases/compiler/jsxElementType.tsx:64:51: 'title' is declared here. | ||
<RenderStringClass title="react" />; | ||
<RenderStringClass excessProp />; | ||
~~~~~~~~~~ | ||
!!! error TS2769: No overload matches this call. | ||
!!! error TS2769: Overload 1 of 2, '(props: Readonly<{ title: string; }>): RenderStringClass', gave the following error. | ||
!!! error TS2769: Type '{ excessProp: true; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<RenderStringClass> & Readonly<{ children?: ReactNode; }> & Readonly<{ title: string; }>'. | ||
!!! error TS2769: Property 'excessProp' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<RenderStringClass> & Readonly<{ children?: ReactNode; }> & Readonly<{ title: string; }>'. | ||
!!! error TS2769: Overload 2 of 2, '(props: { title: string; }, context?: any): RenderStringClass', gave the following error. | ||
!!! error TS2769: Type '{ excessProp: true; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<RenderStringClass> & Readonly<{ children?: ReactNode; }> & Readonly<{ title: string; }>'. | ||
!!! error TS2769: Property 'excessProp' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<RenderStringClass> & Readonly<{ children?: ReactNode; }> & Readonly<{ title: string; }>'. | ||
|
||
// Host element types still work | ||
<div />; | ||
<my-custom-element />; | ||
// Undeclared host element types are still rejected | ||
<boop />; | ||
~~~~~~~~ | ||
!!! error TS2339: Property 'boop' does not exist on type 'JSX.IntrinsicElements'. | ||
<my-undeclared-custom-element />; | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
!!! error TS2339: Property 'my-undeclared-custom-element' does not exist on type 'JSX.IntrinsicElements'. | ||
|
||
// Highlighting various ecosystem compat issues | ||
// react-native-gesture-handler | ||
// https://github.com/software-mansion/react-native-gesture-handler/blob/79017e5e7cc2e82e6467851f870920ff836ee04f/src/components/GestureComponents.tsx#L139-L146 | ||
interface ReactNativeFlatListProps<Item> {} | ||
function ReactNativeFlatList( | ||
props: {}, | ||
ref: React.ForwardedRef<typeof ReactNativeFlatList> | ||
) { | ||
return null; | ||
} | ||
<ReactNativeFlatList />; | ||
~~~~~~~~~~~~~~~~~~~ | ||
!!! error TS2786: 'ReactNativeFlatList' cannot be used as a JSX component. | ||
!!! error TS2786: Its type '(props: {}, ref: ForwardedRef<typeof ReactNativeFlatList>) => null' is not a valid JSX element type. | ||
!!! error TS2786: Type '(props: {}, ref: ForwardedRef<typeof ReactNativeFlatList>) => null' is not assignable to type '(props: any) => React18ReactNode'. | ||
!!! error TS2786: Target signature provides too few arguments. Expected 2 or more, but got 1. | ||
|
||
// testing higher-order component compat | ||
function f1<T extends (props: {}) => React.ReactElement<any>>(Component: T) { | ||
return <Component />; | ||
~~~~~~~~~ | ||
!!! error TS2322: Type '{}' is not assignable to type 'LibraryManagedAttributes<T, {}>'. | ||
} | ||
|
||
<Unresolved />; | ||
~~~~~~~~~~ | ||
!!! error TS2304: Cannot find name 'Unresolved'. | ||
<Unresolved foo="abc" />; | ||
~~~~~~~~~~ | ||
!!! error TS2304: Cannot find name 'Unresolved'. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this would only be for library authors providing a JSX factory of some sort, right? I don't have a strong opinion on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it should be a literal type. The literal type will be assignable to
string
, and forwarding the literal type lets library authors filter the allowed types to only actually existing ones.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh, in fact it has to be a literal if it's called
ElementType
type because Preact already declaresElementType
https://github.com/preactjs/preact/blob/90042a1a012855ef975309485534595705c777c5/src/jsx.d.ts#L34-L40
Which is why the following tests break in DefinitelyTyped.
#51328 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, given that
ElementType
is generic for them, wouldn't we have to instantiate with default arguments? Do you know if that's necessary @weswigham?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kept it as an abstract string since the checker later checked against known host element types in
JSX.IntrinsicElements
anyway and still kept the old error message (see #51328 (comment)).So unknown host elements are still rejected and I think we still need
JSX.IntrinsicElements
anyway for props assignability.I added a test to verify that unknown host element types are still rejected and then switched to
getStringLiteralType(tagName.escapedText as string)
which didn't change any behavior as far as I can tell.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you can do
tagName.escapedText as string
because the escaping won't correctly match up in a case like: