You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于数据拉取状态为useFetch钩子定义正确的返回类型

useFetch Hook 返回值判别联合类型实现问题

沙盒链接:CodeSandbox 示例

现有代码与问题描述

初始类型定义

type TUseFetchLoadingState = {
    loading: true;
    data: never;
    update: never;
    mutate: never;
    error: never;
};

type TUseFetchLoadedState<T> = {
    data: T;
    loading: never;
    update: Dispatch<SetStateAction<T>>;
    mutate: KeyedMutator<T>;
    error: never;
};

type TUseFetchErrorState = {
    error: AxiosError;
    loading: never;
    data: never;
    update: never;
    mutate: never;
};

type TUseFetchReturnType<T> = TUseFetchLoadingState | TUseFetchLoadedState<T> | TUseFetchErrorState;

自定义Hook实现

const useFetch = <T>(url: string | null): TUseFetchReturnType<T> => {
    const { data, error, mutate } = useSWR<T, AxiosError>(url ? url : null);
    const [fetchedData, setFetchedData] = useState<T>();

    useEffect(() => {
        if (data) setFetchedData(data);
    }, [data]);

    if (!fetchedData && !error) {
        return {
            loading: true,
        };
    }

    if (error) {
        return {
            error,
        };
    }

    return {
        data: fetchedData as T,
        update: setFetchedData as Dispatch<SetStateAction<T>>,
        mutate,
    };
};

设计思路

Hook逻辑为:拿到服务端返回数据后存入本地state,更新UI时不需要等待SWR重新校验完成,提升交互响应速度。
预期返回值规则:

  • 无数据且无错误时,返回{ loading: true },支持类型守卫收窄
  • 请求出错时仅返回error对象
  • 请求成功时返回data、本地state更新方法update、SWR重新校验方法mutate

期望的组件使用效果:

const MyComponent = () => {
  const { data, update, loading, error, mutate } = useFetch(ENDPOINTS.SOME_ENDPOINT);
  
  if (error) return <ErrorComponent error={error} />
  if (loading) return <LoadingComponent />
  
  // 此处TS可自动推断data、update、mutate类型可用
  return (
    <Box>
      {/* 基于data渲染内容 */}
    </Box>
  );
}

之前已经实现过符合预期的Props判别联合:

type RequiredTableToolbarProps = {
    title: string;
};

type SelectableTableToolbarProps = RequiredTableToolbarProps & {
    numSelected: number;
    onDeleteSelected: () => void | Promise<void>;
};

type NonSelectableTableToolbarProps = RequiredTableToolbarProps & {
    numSelected: never;
    onDeleteSelected: never;
};

export type TableToolbarProps = SelectableTableToolbarProps | NonSelectableTableToolbarProps;

该类型可实现约束:组件要么同时传入numSelected和onDeleteSelected,要么两个属性都不传入。现在需要给Hook返回值实现同样的判别联合效果。


解决方案

你之前的Props类型能生效,核心是通过互斥的字面量类型标记了不同联合分支的差异,但Hook返回值的类型定义踩了两个关键错误:

  1. 没有给非加载状态的loading字段设置false字面量类型,反而标记为never,TS无法将loading作为判别键做类型收窄
  2. 用never标记当前分支不存在的字段,会导致组件中提前解构所有返回值时TS报错——never意味着属性完全不存在,而常规写法会在判断分支前就解构出所有字段名

修正后的类型定义

把所有分支的公共字段都显式声明,不存在的字段用可选字段 + undefined标记,同时给每个分支的判别字段(loading/error)设置唯一的字面量类型:

import type { Dispatch, SetStateAction } from 'react';
import type { AxiosError } from 'axios';
import type { KeyedMutator } from 'swr';

// 加载中状态:loading为true,其余业务字段值均为undefined
type TUseFetchLoadingState = {
    loading: true;
    data?: undefined;
    update?: undefined;
    mutate?: undefined;
    error?: undefined;
};

// 加载成功状态:loading为false,error为undefined,业务字段完整可用
type TUseFetchLoadedState<T> = {
    loading: false;
    error?: undefined;
    data: T;
    update: Dispatch<SetStateAction<T>>;
    mutate: KeyedMutator<T>;
};

// 加载失败状态:loading为false,业务字段为undefined,仅error可用
type TUseFetchErrorState = {
    loading: false;
    data?: undefined;
    update?: undefined;
    mutate?: undefined;
    error: AxiosError;
};

type TUseFetchReturnType<T> = TUseFetchLoadingState | TUseFetchLoadedState<T> | TUseFetchErrorState;

注意事项

  1. 修正类型后,原Hook逻辑不需要改动,TS会自动校验返回值是否符合类型约束。你之前写的as类型断言是安全的——因为只有加载成功分支会返回update方法,此时fetchedData一定已经有值。
  2. 组件中调用useFetch时建议手动传入泛型参数,避免TS无法自动推断data类型变成unknown,比如useFetch<{name: string}>(ENDPOINTS.SOME_ENDPOINT)。
  3. 这种写法完全支持你预期的类型收窄逻辑:判断error存在时TS会自动定位到错误分支,判断loading为true时定位到加载分支,两个判断都不通过时,TS会自动推断出data/update/mutate字段可用,不会报类型错误。

为什么Props里用never可以,Hook返回值不行?因为Props使用时你不会在做类型判别之前就访问不存在的属性,但Hook返回值通常会先一次性解构所有字段,?: undefined的写法既允许提前解构,又能在类型收窄后排除undefined的类型,完美匹配日常使用习惯。


内容的提问来源于stack exchange,提问作者PYTHON DEVELOPER999

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 23:03:24