'TypeScript + React: Enforce that component returns another type of component
Let's say you have a generic component called <Banner />
:
function Banner({ message }: { message: string; }) {
return <div>{message}</div>;
}
And then let's say I want to create <SuccessBanner />
and <ErrorBanner />
:
function SuccessBanner() {
return <Banner message="success" />;
}
function ErrorBanner() {
return <Banner message="error" />;
}
How can I use typescript to enforce that both SuccessBanner and ErrorBanner return a <Banner />
and not anything else? i.e. something like this
function SuccessBanner(): <Banner /> {
// return <div />; // would throw a TS error
return <Banner message="success" />;
}
function ErrorBanner(): <Banner /> {
return <Banner message="error" />;
}
Solution 1:[1]
Use React.ReactElement<>
i.e.
function SuccessBanner(): React.ReactElement<typeof Banner> {
// return <div />; // would throw a TS error
return <Banner message="success" />;
}
function ErrorBanner(): React.ReactElement<typeof Banner> {
return <Banner message="error" />;
}
Edit: hmm this doesn't seem to be working as I would expect. It seems the value of a component rendered in JSX is JSX.Element
which unfortunately can not be typed to be a specific component.
Solution 2:[2]
This is not (currently) possible. The return type of JSX will always be JSX.Element
, a non generic type.
Explanation
The JSX
namespace provides types and interfaces that tell TS how to interpret JSX: which elements exist; which properties and can be passed along with their respective types; the shape of component functions and classes; and, most relevant here, the return type of the JSX Factory Function.
JSX Factory Function
tsc
has an option named jsxFactory
which takes a string
. In react, this is set to React.createElement
(which also happens to be the default value for this property). This property gives the name of a function that is called anytime JSX is used. JSX implementers can leverage this to provide a custom JSX factory function like this oversimplification:
// Example for using JSX to create DOM elements:
function jsx(tag: JSX.Tag | JSX.Component,
attributes: { [key: string]: any } | null,
...children: Node[])
{
// Handle components:
if (typeof tag === 'function') {
return tag(attributes ?? {}, children);
}
type Tag = typeof tag;
const element: HTMLElementTagNameMap[Tag] = document.createElement(tag);
// ... here we assign properties and add children ...
return element;
}
Then all we have to do is set "jsxFactory": "jsx"
in our tsconfig and import jsx
in any .tsx file (see also: Typescript JSX without React).
So, it seems like we should be able to do some type inference with generics on our jsx
function to get specific return types, right?
JSX.Element
Wrong. Unfortunately, Typescript takes a bit of a shortcut (something about avoiding compiler slowdowns due to deep nested JSX); rather than infer the return type of JSX using the factory function, Typescript uses the JSX.Element
type. This type is not generic: it can be any one thing:
namespace JSX {
type Element = Node;
}
So, what is React's definition of JSX.Element
?
namespace JSX {
interface Element extends React.ReactElement<any, any> { }
// ...
So every JSX.Element
will be React.ReactElement<any, any>
, regardless of elements or components.
In your example, we can verify this like so:
type BannerType = ReturnType<typeof Banner>;
// Intellisense: type BannerType = JSX.Element
const div = <div />
type OtherType = typeof div;
// Intellisense: type OtherType = JSX.Element
Note: At the time of writing, Typescript's version is 4.6.2.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | Connor Low |