react-apollo-hooks icon indicating copy to clipboard operation
react-apollo-hooks copied to clipboard

Typings: data depending on error and loading

Open codepunkt opened this issue 5 years ago • 10 comments

If there is no error and loading is false, is it even possible to have undefined data?

The example given in the Readme is not possible in typescript, because data might be undefined in the final return statement so we can't map over data.dogs.

It would be nice if the typings could reflect this.

const Dogs = () => {
  const { data, error, loading } = useQuery(GET_DOGS);
  if (loading) {
    return <div>Loading...</div>;
  };
  if (error) {
    return <div>Error! {error.message}</div>;
  };

  return (
    <ul>
      {data.dogs.map(dog => (
        <li key={dog.id}>{dog.breed}</li>
      ))}
    </ul>
  );
};

codepunkt avatar Mar 07 '19 08:03 codepunkt

Not taking partials and errors into account, i was thinking about something along these lines (using discriminated unions, simplified example):

interface QueryHookStateLoading {
  data: undefined
  loading: true
  error: undefined
  networkStatus: NetworkStatus | undefined
}

interface QueryHookStateFailure {
  data: undefined
  loading: false
  error: ApolloError
  networkStatus: NetworkStatus | undefined
}

interface QueryHookStateSuccess<T> {
  data: T
  loading: false
  error: undefined
  networkStatus: NetworkStatus | undefined
}

type QueryHookState<T> =
  | QueryHookStateLoading
  | QueryHookStateFailure
  | QueryHookStateSuccess<T>

If something like this was possible, it would clearly help! 👍

codepunkt avatar Mar 07 '19 08:03 codepunkt

I think that data is always TData | {} in Apollo. Would it be best to do this too here?

tleunen avatar Mar 11 '19 17:03 tleunen

@tleunen there is a case when data is undefined - when the query is skipped (it's implemented the same way in react-apollo).

@codepunkt unfortunately it couldn't be implemented that way - there is a case when loading is true and data is T - when fetchMore was invoked and hadn't finished loading additional data. Long term (when Suspense for data fetching will be ready) I'd change the API as described in https://github.com/trojanowski/react-apollo-hooks/issues/28#issuecomment-467191957 - it would solve the problem.

trojanowski avatar Mar 25 '19 13:03 trojanowski

But only when skip is actually provided, isn't it?

tleunen avatar Mar 25 '19 13:03 tleunen

I think that data is always TData | {} in Apollo. Would it be best to do this too here?

I'm using typescript but my IDE hinted me that the data type is TData | undefined. But in reality its TData | {}. Why useQuery gives me TData | undefined instead of TData | {}?

jauhararifin avatar Apr 03 '19 00:04 jauhararifin

Because if the type would be TData | {}, you would have to declare a custom type guard for the type {} instead of just writing if(!data).

danielbischoff avatar Apr 03 '19 09:04 danielbischoff

But isn't an empty object truthy not falsy so you can't really just write if (!data) ...?

const data = {};

if (!data) {
  console.log("no data");
} else {
  console.log("data exists"); // always the case
}

So code like this makes TypeScript happy but you actually get a runtime error, because initially data is {} and thus accessing name from undefined data.user gives error.

export const App: React.FC = () => {
  const { data } = useQuery<User>(GET_USER);

  if (!data) {
    return <LoadingView />;
  }

  return <div>Name: {data.user.name}</div>;
};

This would work if the data was actually undefined not an empty object (as undefined is falsy).

kallaspriit avatar Apr 30 '19 13:04 kallaspriit

export const App: React.FC = () => {
  const { data } = useQuery<User>(GET_USER);

  if (!data) {
    return <LoadingView />;
  }

  return <div>Name: {data.user.name}</div>;
};

This is the perfect example. It won't work like this.

return <div>Name: {data.user.name}</div>;

This line is the problem, as the type for data is either {} or TData (so you define a union). Typescript will complain at compile time about the access to data.user, because data can be of type {} or TData. Thus you need typeguards in your code. https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types Please don't mix {} used as a value and {} used as a type. I think that is what you are doing here.

danielbischoff avatar May 05 '19 08:05 danielbischoff

Not sure that I follow what is the correct way to handle this then?

The problem is that the typings and actual data are different right.

When loading, the actual data is an empty object {} but the typings say that it's either TData | undefined. So when I know check for undefined (or falsyness as shown in the example), TypeScript thinks it's safe to use properties from this object when in reality one would get a runtime error.

I understand that if the typings were fixed to match (TData | {}), one would need a cumbersome type guard.

Better fix would be to make the hook return undefined instead of an empty object so the typings would match reality and the falsyness check would work as expected.

kallaspriit avatar May 06 '19 10:05 kallaspriit

Having trouble coming up with a suitable type guard for this?

elmpp avatar Sep 04 '19 07:09 elmpp