TypeScript泛型实现useFetch及Context传值类型报错问题
TypeScript 泛型useFetch Hook 类型报错修复方案
核心报错原因
- 初始Hook类型定义结构不一致:你声明的
useFetchState<T>是{ data: T | null }的对象结构,但useState初始值传入null,后续setData又直接存入接口返回的原始数据(没有包裹{data: xxx}层),类型完全不匹配,直接导致前两个linter报错。 - Context联合类型未收窄:Context中
data定义为movieApi | movieObject | null联合类型,movieObject(单条电影详情)不存在results字段,TS无法自动判断当前data的具体类型,因此访问results时提示属性不存在。 - 错误类型不匹配:自定义抛出的请求错误是对象结构,和
error字段声明的string | null类型冲突。
分步修复代码
1. 重构useFetch Hook,修正类型匹配
删除多余的useFetchState嵌套包装,统一state结构和错误类型:
import { useEffect, useState } from "react" // 统一请求错误类型 type FetchError = { err: boolean; statuscode: number; message: string; } | null; export const useFetch = <T>(url: string) => { // 直接存储接口返回数据,类型为T | null,不再套多余的对象层 const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState<boolean>(false); const [error, setError] = useState<FetchError>(null); const getData = async (fetchUrl: string) => { setLoading(true); setError(null); try { const res = await fetch(fetchUrl); if (!res.ok) { throw { err: true, statuscode: res.status, message: "Error in your request" }; } const resData = await res.json(); setData(resData); } catch (err) { setError(err as FetchError); } finally { setLoading(false); } } useEffect(() => { getData(url) }, [url]) return { data, loading, error } }
如果要保留原Promise链式写法也可,只需保证
setData传入的是接口返回的原始数据,state类型声明和实际存储值结构一致即可。
2. 处理Context联合类型的访问报错
两种方案可选,优先推荐第二种:
方案A:添加类型守卫做类型收窄
在类型定义文件中增加类型判断函数,访问属性前先校验数据结构:
// interface文件中新增 import type { movieApi } from "./interface"; // 类型守卫:判断当前数据为电影列表结构 export const isMovieList = (data: unknown): data is movieApi => { return typeof data === "object" && data !== null && "results" in data; }
Home组件使用时先做类型判断:
import { movieApiResult, isMovieList } from "../../hooks/useFetch/interface"; import { useMovies } from "../../context/MoviesContext"; import "./home.css"; const Home = () => { const IMGPATH = "https://image.tmdb.org/t/p/w1280"; const { data, loading } = useMovies(); return ( <div className="container"> {loading && <div>加载中</div>} {isMovieList(data) && data.results.map((item: movieApiResult) => ( <div key={item.id} className="item"> <img src={`${IMGPATH}${item.poster_path}`} alt={item.title} className="image" /> <p>{item.title}</p> <span>{item.vote_average}</span> </div> ))} </div> ); }; export default Home;
方案B:拆分Context状态(推荐)
不要把两种结构完全不同的接口返回值存在同一个data字段,拆分独立状态从根源避免联合类型问题,修改Context类型定义:
interface MoviesTypes { listData: movieApi | null; detailData: movieObject | null; loading: boolean; setUrl: Dispatch<SetStateAction<string>>; handleClick: (id: number) => void; IMGPATH: string; }
列表页取listData访问results字段,详情页取detailData访问单条电影属性,无需额外类型判断,可维护性更高。
内容的提问来源于stack exchange,提问作者Facu
相关产品推荐
相关产品推荐

