Search code examples
reactjstypescript

How to handle undefined values with RenderIf component similar to && check


I am trying to move over to TypeScript a pattern I used in React with JavaScript of a RenderIf component that checked whether it should show the content below or return null. The following code does work in TypeScript when not passing a potentially undefined value. I don't know how to help the TypeScript compiler realize that we already did the && check to validate that the value exists if the value can be undefined.

RenderIf component:

type RenderIfProps = {
  condition: boolean;
};

export const RenderIf: React.FC<RenderIfProps> = (props) => {
  const { condition = false, children } = props;
  const isConditionMetAndHasChildren = condition === true && !!children;

  if (isConditionMetAndHasChildren) {
    return <>{children}</>;
  }
  return null;
};

Example of TypeScript not being happy with RenderIf:

const someComponent = (props: { someValue?: string }) => {
  return (
    <RenderIf condition={!!props?.someValue}>
      <ComponentA definedValue={props.someValue} />
    </RenderIf>
  )
}

Example of TypeScript being happy with &&:

const someComponent = (props: { someValue?: string }) => {
  return (
    !!props?.someValue && <ComponentA definedValue={props.someValue} />
  )
}

This is the error message:

TS18049:  props  is possibly  null  or  undefined

How might I fix this?


Solution

  • Why doesn't it work? Because that's not how TypeScript works.


    The props.someValue typing will always be the same on the same scope. In this case, the RenderIf and ComponentA functional components are in the same scope.

    function SomeComponent(props: { someValue?: string }) {
      return (
        <RenderIf condition={!!props?.someValue}>
          <ComponentA definedValue={props.someValue} />
        </RenderIf>
      );
    }
    

    That's why definedValue props on ComponentA get this error: Type 'string | undefined' is not assignable to type 'string'.


    Solution 1 - Children Structure

    To make it work, we need to define the typescript scope of the functional component. If you want to keep the children-like structure like your example, I can share a solution to this.

    1. We need to adjust the RenderIf component:

    type RenderIfProps = {
      definedValue?: string;
      children(definedValue: string): ReactElement;
    };
    function RenderIf(props: PropsWithChildren<RenderIfProps>) {
      const { definedValue, children } = props;
    
      if (!definedValue || !children) {
        return null;
      }
    
      return children(definedValue);
    }
    

    I adjusted the RenderIf functional component to:

    • Accept definedValue string | undefined (not the boolean state)
    • Re-define the children props from accepting a component to a function, this way I can change the scope of the typescript into the typing that I want.

    2. We need to adjust SomeComponent when calling RenderIf

    function SomeComponent(props: { someValue?: string }) {
      return (
        <RenderIf definedValue={props?.someValue}>
          {(value) => <ComponentA definedValue={value} />}
        </RenderIf>
      );
    }
    

    I change how I call the children inside the RenderIf component. Instead of directly adding a component, I add a function, in which the parameter is a string type, not string | undefined like before. This is what I previously called changing the scope of the typing.

    CodeSandbox example: here


    I put the most similar solution to your example. Depending on the use case, this solution might not be the best. And since there are a lot of ways to achieve what you want, I put the "Solution 1" above.

    Let me know if it doesn't work for you.