Prop required or optional based on other boolean props in this case `isLoading` in typescript

454 Views Asked by At

I'm trying to create a component in ReactJS and manage prop types using typescript

function AccountInfoCard(props: AccountInfoCardProps) {
  const { data, isLoading } = props;

  if (isLoading) {
    return <Text>Loading...</Text>;
  }

  // In this area isLoading must be false and data will be provided correctly.
// no need for optional chaining like this data?.balance
  return <Text>{data.balance}</Text>;
}

the current type looks like this


type Data = {
  balance: number;
};

type AccountInfoCardProps = {
  isLoading?: boolean;
  data?: Data;
};

And i'm expecting that the below requirement to be valid and working as I expected as described in the comment

function Test() {
  const dataFromApi = { balance: 100 } satisfies Data;
  const isLoadingApi = 1 + 1 === 2;

  return (
    <>
      {/* // Error: Either isLoading or data must be provided */}
      <AccountInfoCard />
      {/* // Error: Cannot set isLoading to false and not provide data */}
      <AccountInfoCard isLoading={false} />

      {/* // Valid: isLoading provided with boolean, data becomes optional */}
      <AccountInfoCard isLoading={isLoadingApi} />
      {/* // Valid: data is provided then isLoading becomes optional */}
      <AccountInfoCard data={dataFromApi} />
      {/* // Valid: Both isLoading and data are provided */}
      <AccountInfoCard isLoading={isLoadingApi} data={dataFromApi} />
      {/* // Valid: isLoading provided with boolean, data is optional */}
      <AccountInfoCard isLoading={true} />
      {/* // Valid: isLoading provided with boolean and data is also provided */}
      <AccountInfoCard isLoading={isLoadingApi} data={dataFromApi} />
    </>
  );
}

what i have tried

type AccountInfoCardProps = {
  isLoading: boolean;
  data?: Data;
} | {
  isLoading?: false;
  data: Data;
};

I tried discriminated union like above code but one condition doesn't fulfilled which is

 {/* // Error: Cannot set isLoading to false and not provide data */}
      <AccountInfoCard isLoading={false} />

suppose to be error but it is not throwing any error

1

There are 1 best solutions below

0
Slava Knyazev On

The solution here comes in two parts. First, you need to declare the two possible AccountInfoCardProps as a union, as you have done:

type AccountInfoCardProps = {
  isLoading?: false;
  data: Data;
} | {
  isLoading: true;
  data?: undefined;
};

but after doing that, simply writing <AccountInfoCard isLoading={someBoolean} /> will not work.

This is because in order to let TS pick one of the members of the union, you need to prove whether the boolean is true or false:

// This works
{isLoadingApi && <AccountInfoCard isLoading={isLoadingApi} />}

The end result looks like this: Playground

function Test() {
  const dataFromApi = { balance: 100 } satisfies Data;
  const isLoadingApi = 1 + 1 === 2;

  return (
    <>
      {/* // Error: Either isLoading or data must be provided */}
      <AccountInfoCard />
      {/* // Error: Cannot set isLoading to false and not provide data */}
      {isLoadingApi || <AccountInfoCard isLoading={false} />}

      {/* // Valid: isLoading provided with boolean, data becomes optional */}
      {isLoadingApi && <AccountInfoCard isLoading={isLoadingApi} />}
      {/* // Valid: data is provided then isLoading becomes optional */}
      <AccountInfoCard data={dataFromApi} />
      {/* // Valid: Both isLoading and data are provided */}
      {isLoadingApi || <AccountInfoCard isLoading={isLoadingApi} data={dataFromApi} />}
      {/* // Valid: isLoading provided with boolean, data is optional */}
      <AccountInfoCard isLoading={true} />
      {/* // Valid: isLoading provided with boolean and data is also provided */}
      {isLoadingApi || <AccountInfoCard isLoading={isLoadingApi} data={dataFromApi} />}
    </>
  );
}