如何基于数据拉取状态为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返回值的类型定义踩了两个关键错误:
- 没有给非加载状态的
loading字段设置false字面量类型,反而标记为never,TS无法将loading作为判别键做类型收窄 - 用
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;
注意事项
- 修正类型后,原Hook逻辑不需要改动,TS会自动校验返回值是否符合类型约束。你之前写的
as类型断言是安全的——因为只有加载成功分支会返回update方法,此时fetchedData一定已经有值。 - 组件中调用
useFetch时建议手动传入泛型参数,避免TS无法自动推断data类型变成unknown,比如useFetch<{name: string}>(ENDPOINTS.SOME_ENDPOINT)。 - 这种写法完全支持你预期的类型收窄逻辑:判断
error存在时TS会自动定位到错误分支,判断loading为true时定位到加载分支,两个判断都不通过时,TS会自动推断出data/update/mutate字段可用,不会报类型错误。
为什么Props里用never可以,Hook返回值不行?因为Props使用时你不会在做类型判别之前就访问不存在的属性,但Hook返回值通常会先一次性解构所有字段,?: undefined的写法既允许提前解构,又能在类型收窄后排除undefined的类型,完美匹配日常使用习惯。
内容的提问来源于stack exchange,提问作者PYTHON DEVELOPER999
相关产品推荐
相关产品推荐

