reactjstypescriptreact-16

Is there a typing for React Function components that includes returning fragments, nulls, strings etc?


My question is a clarification/update on this highly upvoted question:

When to use JSX.Element vs ReactNode vs ReactElement?

It seems to me that TypeScript is not going to play well when you want to have function components that return primitives like string, null etc.

For example, say you have a component that wants a render prop:

import React from "react"; 

type User = {
    id: string; 
    name: string; 
    imgSrc: string; 
}


type UserProfilePageProps = {
    RenderUserPanel: React.ComponentType<{user: User}>; 
}

export const fetchUser = async () : Promise<User> => {
    return {
        id: "123", 
        imgSrc: "placeholdit", 
        name: "Joe Bloggs"
    }
}


const UserProfilePage =  (props: UserProfilePageProps) => {
    const {RenderUserPanel} = props; 
    const [user, setUser] = React.useState<null |User>(null); 

    React.useEffect(()=> {
        fetchUser().then(user => setUser(user)); 
    }, []);

    return <section>
        <h2>User Details</h2>
        {user && <RenderUserPanel user = {user}/>}
    </section> 
}

Now I can create components that should fulfil this render prop in four different ways:

class ClassBasedUserPanel extends React.Component<{user: User}> {
    render() {
        return <div> 
                {this.props.user.name}
        </div>
    }
}

class ClassBasedUserPanelThatReturnsString extends React.Component<{user: User}> {
    render() {
        return  this.props.user.name; 
    }
}

const FunctionBasedUserPanel = (props: {user: User}) => {
    return <div> 
      {props.user.name}
    </div>
}

const FunctionBasedUserPanelThatReturnsString = (props: {user: User}) => {
    return  props.user.name;
}

However, that FunctionBasedUserPnelThatReturnsAString does not fit React.ComponentType<{user: User}>; because React.FunctionComponent must return a React.ReactElement which a string is not.

React.ClassComponent on the otherhand, does not have this constraint.

const Main = () => {
    return <div> 
        <UserProfilePage RenderUserPanel = {ClassBasedUserPanel}/>
        <UserProfilePage RenderUserPanel = {ClassBasedUserPanelThatReturnsString}/>
        <UserProfilePage RenderUserPanel = {FunctionBasedUserPanel}/>
        
        {/* ERROR! */}
        <UserProfilePage RenderUserPanel = {FunctionBasedUserPanelThatReturnsString}/>

    </div> 
}

Essentially, what I want is a type that is:


type TypeImLookingFor<P = {}>  = React.ComponentType<P> | (props: P) => ReactNode; 

Does this exist? Is there a reason that it shouldn't exist?

Note: I'm currently using React 16. Perhaps this is something that is fixed in React 17 or 18.


Solution

  • I'm going to post my own best solution for React 16 for this now - but I welcome other answers, especially if there are updates from React 17/18.

    The problem with widening the type as I've suggested, is that it doesn't actually work.

    As an example:

    type TypeImLookingFor<P = {}>  = React.ComponentType<P> | ((props: P) => React.ReactNode); 
    
    class ComponentA extends React.Component {
        render() {
            return "aaa"; 
        }
    }
    
    const ComponentB = () => {
        return "bbb"; 
    }
    
    // They do match the type signature
    const A: TypeImLookingFor = ComponentA; 
    const B: TypeImLookingFor = ComponentB; 
    
    const Main =  () => {
        return <div> 
            <ComponentA/> 
            
            {/* 
            'ComponentB' cannot be used as a JSX component.
            Its return type 'string' is not a valid JSX element.ts(2786) */}
            <ComponentB/>
        </div>;
    
    }
    

    That is, if you want use a function component in a 'JSX-y' fashion - you can't return a string, number etc, even though you could do that with a class component.

    The best solution then, in my opinion is:

    1. Explicitly state the return type of your function components as React.ReactElement
    2. If they are returning a string, or a number etc, wrap it in a fragment
    const ComponentC = () : React.ReactElement => {
        //Type 'string' is not assignable to type 'ReactElement<any, string | JSXElementConstructor<any>>'.ts(2322)
        return "aaa"; 
    }
    
    const ComponentD = () : React.ReactElement => {
        return <> aaa </>
    }
    
    const Main2 =  () => {
        return <div> 
            <ComponentA/> 
            <ComponentD/>
        </div>;
    
    }