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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 06:34:01