Typescript - return type conditions

Viewed 137

This question is focused on Typescript usage in React.

I have two React hooks: useStatus(): Status and useResult(): Result | null and use them like:

const status = useStatus();
const result = useResult();

if (status !== "success") {
  return <Loading />;
}

return ... something using result ...;

I can change the signature of both functions (parameters and return type). I can't change the position of the useResult call (it has to be before if) - that's React's only constraint.

Conceptually, I know result will not be null if status is "success". I'd like to somehow encode this information in return type. That way, Typescript would somehow know at last line, that status is "success" so result is Result (not Result | null).

I know I can separate in two different components - that's what I'm already doing, but curious if I can avoid that.

Is there a way to achieve this?

Thanks! :)

1 Answers

A first refactor could be to extract the handling of those two hooks into a dedicated hook with it's own return type.

Afterwards you might (if possible) follow the advice in the comments and move whatever useStatus and useResult do directly into that new hook.

For example:

// mock types
type Status = 'loaded' | 'loading';
type Result = unknown;

// hooks per your description
declare function useStatus(): Status;
declare function useResult(): Result | null;

// new type
// ref: https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#discriminating-unions
type Response =
  | { status: 'loaded'; result: Result; }
  | { status: 'no-result'; }
  | { status: 'loading'; };

// new hook
const useResponse = (): Response => {
  const status = useStatus();
  const result = useResult();

  if ('loaded' === status) {
    return null === result
      ? {status: 'no-result'}
      : {status, result};
  }

  return {status: 'loading'};
};

Using this hook will allow you to savely access it's return properties:

const App: React.FC = () => {
  const response = useResponse();

  switch (response.status) {
    case 'loading':
      return <div>Loading</div>;

    case 'no-result':
      return <div>Failure...</div>;

    case 'loaded':
      return <WithResult result={response.result}/>;
  }
};

Ref: https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#discriminating-unions

Related