'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