0

I'm building a generic useFetch hook, this hook will return an object that depends on the success or failure of the data fething. I want this hook to return the appropriate dynamic type.

This is the type I created:


type Status = "loading" | "success" | "error";

interface FetchBase<TData = unknown> {
  status: Status;
  isLoading: boolean;
  isError: boolean;
  isSuccess: boolean;

  data: TData | undefined;
  error: undefined | string;
}

interface FetchLoading<TData = unknown> extends FetchBase<TData> {
  status: "loading";
  isLoading: true;
  isError: false;
  isSuccess: false;

  data: undefined;
  error: undefined;
}

interface FetchError<TData = unknown> extends FetchBase<TData> {
  status: "error";
  isLoading: false;
  isError: true;
  isSuccess: false;

  data: undefined;
  error: string;
}

interface FetchSuccess<TData = unknown> extends FetchBase<TData> {
  status: "success";
  isLoading: false;
  isError: false;
  isSuccess: true;

  data: TData;
  error: undefined;
}

type FetchResult<TData> =
  | FetchLoading<TData>
  | FetchError<TData>
  | FetchSuccess<TData>;

And this is the hook I mean (useFetch):

function useFetch<TData>(
  fetchFn: () => Promise<AxiosResponse<TData>>
): FetchResult<TData> {
  const [status, setStatus] = useState<Status>("loading");
  const [error, setError] = useState<string>();
  const [data, setData] = useState<TData>();

  useEffect(() => {
    const initFetch = async () => {
      try {
        const res = await fetchFn();
        setData(res.data);
        setStatus("success");
      } catch (error: any) {
        setError(error.message);
        setStatus("error");
      }
    };

    initFetch();

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // error
  return {
    status,
    isLoading: status === "loading",
    isError: status === "error",
    isSuccess: status === "success",
    data,
    error,
  };
}

The problem is TypeScript complains because of the object I return from the useFetch hook, this is the error I get:

Type '{ status: Status; isLoading: boolean; isError: boolean; isSuccess: boolean; data: TData | undefined; error: string | undefined; }' is not assignable to type 'FetchResult<TData>'.
  Type '{ status: Status; isLoading: boolean; isError: boolean; isSuccess: boolean; data: TData | undefined; error: string | undefined; }' is not assignable to type 'FetchSuccess<TData>'.
    Types of property 'status' are incompatible.
      Type 'Status' is not assignable to type '"success"'.
        Type '"loading"' is not assignable to type '"success"'.ts(2322)

I do this because I don't want to do state data checking, it should be if loading = false then the data or error is there. It works fine when I try in React component:

function TestComponent() {
  const { isLoading, isError, error, data } = useFetch<{ name: string }>(() =>
    axios.get("/user")
  );

  if (isLoading) return <p>Loading...</p>;

  if (isError) return <p>{error}</p>;

  // i don't need to do something like below (if(data) ....):
  // if(data) return data.name;

  return <p>{data.name}</p>;
}

1 Answer 1

1

THe most easier way to fix it, is to overload your function:

import React, { useState, useEffect } from 'react'
import { AxiosResponse } from 'axios'

type Status = "loading" | "success" | "error";

interface FetchBase<TData = unknown> {
    status: Status;
    isLoading: boolean;
    isError: boolean;
    isSuccess: boolean;

    data: TData | undefined;
    error: undefined | string;
}

interface FetchLoading<TData = unknown> extends FetchBase<TData> {
    status: "loading";
    isLoading: true;
    isError: false;
    isSuccess: false;

    data: undefined;
    error: undefined;
}

interface FetchError<TData = unknown> extends FetchBase<TData> {
    status: "error";
    isLoading: false;
    isError: true;
    isSuccess: false;

    data: undefined;
    error: string;
}

interface FetchSuccess<TData = unknown> extends FetchBase<TData> {
    status: "success";
    isLoading: false;
    isError: false;
    isSuccess: true;

    data: TData;
    error: undefined;
}

type FetchResult<TData> =
    | FetchLoading<TData>
    | FetchError<TData>
    | FetchSuccess<TData>;

function useFetch<TData>(
    fetchFn: () => Promise<AxiosResponse<TData>>
): FetchResult<TData>
function useFetch<TData>(
    fetchFn: () => Promise<AxiosResponse<TData>>
) {
    const [status, setStatus] = useState<Status>("loading");
    const [error, setError] = useState<string>();
    const [data, setData] = useState<TData>();

    useEffect(() => {
        const initFetch = async () => {
            try {
                const res = await fetchFn();
                setData(res.data);
                setStatus("success");
            } catch (error: any) {
                setError(error.message);
                setStatus("error");
            }
        };

        initFetch();

    }, []);

    return {
        status,
        isLoading: status === "loading",
        isError: status === "error",
        isSuccess: status === "success",
        data,
        error,
    };
}

Playground

But, if you want to make it safer you need to create a custom typeguard to make sure that return value satisfies one of three allowed states:

import React, { useState, useEffect } from 'react'
import { AxiosResponse } from 'axios'

type Status = "loading" | "success" | "error";

interface FetchBase<TData = unknown> {
    status: Status;
    isLoading: boolean;
    isError: boolean;
    isSuccess: boolean;

    data: TData | undefined;
    error: undefined | string;
}

interface FetchLoading<TData = unknown> extends FetchBase<TData> {
    status: "loading";
    isLoading: true;
    isError: false;
    isSuccess: false;

    data: undefined;
    error: undefined;
}

interface FetchError<TData = unknown> extends FetchBase<TData> {
    status: "error";
    isLoading: false;
    isError: true;
    isSuccess: false;

    data: undefined;
    error: string;
}

interface FetchSuccess<TData = unknown> extends FetchBase<TData> {
    status: "success";
    isLoading: false;
    isError: false;
    isSuccess: true;

    data: TData;
    error: undefined;
}

type FetchResult<TData> =
    | FetchLoading<TData>
    | FetchError<TData>
    | FetchSuccess<TData>;


const isLoading = <T,>(response: FetchBase<T>): response is FetchLoading<T> => {
    const {
        status,
        isLoading,
        isError,
        isSuccess,
        data,
        error
    } = response;
    return (
        status === 'loading'
        && isLoading
        && !isError
        && !isSuccess
        && data === undefined
        && error === undefined
    )
}

const isError = <T,>(response: FetchBase<T>): response is FetchError<T> => {
    const {
        status,
        isLoading,
        isError,
        isSuccess,
        data,
        error
    } = response;
    return (status === 'error'
        && !isLoading
        && isError
        && !isSuccess
        && data === undefined
        && typeof error === 'string'
    )
}

const isSuccess = <T,>(response: FetchBase<T>): response is FetchSuccess<T> => {
    const {
        status,
        isLoading,
        isError,
        isSuccess,
        data,
        error
    } = response;
    return (status === 'success'
        && !isLoading
        && !isError
        && isSuccess
        && data !== undefined
        && data !== null
        && error === undefined
    )
}

function useFetch<TData>(
    fetchFn: () => Promise<AxiosResponse<TData>>
): FetchResult<TData> | null {
    const [status, setStatus] = useState<Status>("loading");
    const [error, setError] = useState<string>();
    const [data, setData] = useState<TData>();

    useEffect(() => {
        const initFetch = async () => {
            try {
                const res = await fetchFn();
                setData(res.data);
                setStatus("success");
            } catch (error: any) {
                setError(error.message);
                setStatus("error");
            }
        };

        initFetch();

    }, []);
    const result = {
        status,
        isLoading: status === "loading",
        isError: status === "error",
        isSuccess: status === "success",
        data,
        error,
    };

    if (isLoading(result) || isError(result) || isSuccess(result)) {
        return result
    }

    return null
}

Playground

There is still no 100% guarantee that result will be allowed state, this is why I think it worth returning null when for some reason all these props

        isLoading,
        isError,
        isSuccess,

will be false or true

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for your answer, I learned a lot from your code.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.