news 2026/9/30 6:37:14

SWR + TypeScript 实战指南:用类型安全的 fetcher 与 useSWR 泛型构建数据获取层

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SWR + TypeScript 实战指南:用类型安全的 fetcher 与 useSWR 泛型构建数据获取层
  • 前端
  • 缓存

【免费下载链接】swr

React Hooks for Data Fetching

项目地址:https://gitcode.com/gh_mirrors/sw/swr
点击查看免费下载

导读

本指南基于 SWR 官方仓库中的 basic-typescript 示例,讲解如何用 TypeScript 为 SWR 的数据请求链路引入完整的类型约束:从带泛型的 fetcher 函数,到useSWR声明式地标注响应数据类型,再到 Next.js 动态路由页面的数据结构声明。读完本文,你将掌握在 Next.js + SWR + TypeScript 项目里搭建一条"从接口返回到 UI 渲染全程类型安全"的请求链路的完整做法,并理解 SWR 内部泛型推断的底层原理。

一、示例定位:官方 TypeScript 入门模板

仓库根目录下的 examples/basic-typescript/README.md 明确给出了该示例的核心目标:

Show how to use the basic example along with TypeScript to type the data received from SWR.

即:在 SWR 基础用法之上叠加 TypeScript,为 SWR 获取到的数据提供类型标注。它与纯 JavaScript 版本的 examples/basic 示例形成对照——后者只演示"在两个页面从 API 获取数据"的基本用法,而本示例的全部价值就在于给这条请求链路加上类型。

整个示例是一个最小可运行的 Next.js 应用,由四个文件组成:

文件职责
libs/fetch.ts带泛型的类型安全 fetcher
pages/index.tsx首页,请求项目列表(string[])
pages/[user]/[repo].tsx动态路由页,请求 GitHub 仓库统计数据(对象结构)
pages/api/data.tsNext.js API 路由,模拟慢速接口

下面从运行方式、fetcher 设计、页面数据消费三个层面逐一展开。

二、快速运行:下载、安装与启动

2.1 下载示例

官方 README 提供了一条基于 curl + tar 的下载命令,直接从 SWR 仓库的主分支中剥离出该示例目录:

curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/basic-typescript cd basic-typescript

命令说明:--strip=2会去掉压缩包内swr-main/examples/basic-typescript/的前两级目录,将示例内容直接解压到当前目录。

2.2 安装与启动

README 同时给出了 yarn 与 npm 两种等价方式:

yarn yarn dev # 或 npm install npm run dev

对应到 examples/basic-typescript/package.json 中的 scripts 配置:

"scripts": { "dev": "next", "start": "next start", "build": "next build" }

三个脚本分别对应开发模式(next)、生产运行(next start,需先执行 build)与生产构建(next build)。

2.3 依赖组成

依赖清单展示了该示例的技术栈组合:

"dependencies": { "next": "latest", "react": "latest", "react-dom": "latest", "swr": "latest" }, "devDependencies": { "@types/node": "16.7.2", "@types/react": "17.0.19", "typescript": "4.3.5" }

要点:

  • 运行时依赖全部取最新版,其中swr直接依赖当前仓库的发布版本;
  • TypeScript 相关类型包放在 devDependencies(@types/node、@types/react、typescript),类型标注只影响编译期,不参与运行时打包;
  • 注意这里没有独立的@types/swr,因为 SWR 自带的类型定义已足够完整,这也是"官方示例不需要额外类型包"这一事实的最好证明。

2.4 TypeScript 编译配置

examples/basic-typescript/tsconfig.json 是一份标准的 Next.js + TypeScript 配置,其中与类型安全直接相关的关键项:

"compilerOptions": { "target": "es5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve" }

值得注意的两点:

  • strict: true开启全部严格模式选项,确保 fetcher 与页面中的类型标注真正生效、不留类型盲区;
  • noEmit: true表明该配置仅供类型检查,实际编译产物由 Next.js 的打包链路负责,与next-env.d.ts配合即可满足开发时类型检查需求(next-env.d.ts 由 Next.js 自动维护,官方注明"不应手动编辑")。

三、核心一:带泛型的类型安全 fetcher

3.1 示例中的 fetcher 实现

libs/fetch.ts 是整个示例类型安全的起点:

export default async function fetcher<JSON = any>( input: RequestInfo, init?: RequestInit ): Promise<JSON> { const res = await fetch(input, init) return res.json() }

逐行拆解:

  • <JSON = any>泛型默认值:fetcher 声明了一个名为JSON的泛型(名字取自"JSON 响应体"的语义,与内置JSON对象并无冲突),默认值为any。这意味着:调用方若不指定类型,行为退化为普通 JavaScript;一旦调用方通过泛型或上下文推断给出类型,整个返回值的Promise包装层就拥有了精确类型;
  • 参数类型:input: RequestInfo与init?: RequestInit直接复用 DOM 标准类型(即fetch()原生签名),因此这个 fetcher 能无缝兼容任意 URL 字符串或Request对象;
  • 返回值:Promise<JSON>,即"响应体反序列化结果"被类型化为调用方声明的数据类型。

这段实现与其 JavaScript 版对照物 examples/basic/libs/fetch.js 在运行时行为上完全一致(都是fetch → res.json()),差异全部集中在类型层——这正是"同样的逻辑,多一份类型保障"的直观体现。

3.2 泛型是如何向下流动的:Fetcher 类型定义

从源码层面看,SWR 内部用条件类型为 fetcher 建立了"由 Key 推导参数类型"的约束。在 src/_internal/types.ts 中:

export type Fetcher< Data = unknown, SWRKey extends Key = Key > = SWRKey extends () => (infer Arg) | null | undefined | false ? (arg: Arg) => FetcherResponse<Data> : SWRKey extends null | undefined | false ? never : SWRKey extends infer Arg ? (arg: Arg) => FetcherResponse<Data> : never

这套条件类型表达了三条规则:

  1. Key 是函数:若 Key 是返回值的函数(如() => '/api/data'),fetcher 的参数类型就是该函数的返回值类型Arg;
  2. Key 是空值:若 Key 是null | undefined | false,fetcher 类型为never——因为此时 SWR 根本不会发起请求(对应useSWR(null, fetch)的"禁用请求"场景);
  3. Key 是普通值:否则 fetcher 的参数类型就是 Key 本身。

FetcherResponse<Data>(定义于 src/_internal/types.ts)即Data | Promise<Data>,允许 fetcher 返回数据本身或一个 Promise。因此示例中fetcher的Promise<JSON>返回类型天然满足 SWR 的FetcherResponse约束。

3.3 从useSWR泛型到响应类型

SWR 的公开入口 src/index/index.ts 将 src/index/use-swr.ts 中的useSWRHandler以useSWR<Data = any, Error = any>的签名导出。Data与Error两个泛型会一路传递到 hook 的返回值SWRResponse<Data, Error>中,最终让data、error、mutate等都拥有对应类型。

这意味着使用方只需要在调用useSWR时声明Data泛型(通常配合fetcher的返回类型),就能让编译器在data.xxx访问处做完整的属性存在性校验——这正是下一节页面代码演示的用法。

四、核心二:页面中的类型化数据消费

4.1 首页:请求string[]列表

pages/index.tsx 展示了最简单的类型标注:

import Link from 'next/link' import fetch from '../libs/fetch' import useSWR from 'swr' export default function HomePage() { const { data } = useSWR<string[]>('/api/data', fetch) const { data: data2 } = useSWR(null, fetch) return ( <div style={{ textAlign: 'center' }}> <h1>Trending Projects</h1> {data2} <div> {data ? data.map(project => ( <p key={project}> <Link href="/[user]/[repo]" as={`/${project}`}> {project} </Link> </p> )) : 'loading...'} </div> </div> ) }

两个值得深入解读的细节:

细节一:useSWR<string[]>显式泛型

useSWR<string[]>('/api/data', fetch)告诉 SWR:/api/data返回的是字符串数组。于是:

  • data.map(project => ...)中project被推断为string,key={project}与 Link 的as={/${project}}都获得类型校验;
  • 渲染分支data ? ... : 'loading...'中,data被收窄为非空数组,这正是 SWR 的data在无缓存时可能为undefined的运行时事实在类型层的反映。

细节二:useSWR(null, fetch)的条件请求写法

第二行useSWR(null, fetch)把 Key 显式设为null,这是 SWR 官方的"禁用请求"惯用法。回到 src/_internal/types.ts 的Fetcher条件类型,null会让 fetcher 类型变为never,类型系统因此知道"这个 hook 永远不会发起网络请求"。同时data2类型为string | undefined(本示例中未赋值,实际渲染为空)。这种写法常用于"等待用户输入/条件满足后再请求"的场景。

4.2 动态路由页:请求结构化对象

pages/[user]/[repo].tsx 演示了对复杂对象结构的类型化:

import Link from 'next/link' import fetch from '../../libs/fetch' import useSWR from 'swr' export default function Repo() { const id = typeof window !== 'undefined' ? window.location.pathname.slice(1) : '' const { data } = useSWR<{ forks_count: number stargazers_count: number watchers: number }>('/api/data?id=' + id, fetch) return ( <div style={{ textAlign: 'center' }}> <h1>{id}</h1> {data ? ( <div> <p>forks: {data.forks_count}</p> <p>stars: {data.stargazers_count}</p> <p>watchers: {data.watchers}</p> </div> ) : ( 'loading...' )} <br /> <br /> <Link href="/">Back</Link> </div> ) }

解读:

  • 内联对象类型:泛型参数直接写为对象字面量类型{ forks_count: number; stargazers_count: number; watchers: number },无需额外定义 interface。访问data.forks_count等字段时,编译器会校验属性是否存在且类型为number;
  • 模板字符串 Key:'/api/data?id=' + id使 Key 携带查询参数,SWR 会为每个不同的id建立独立缓存条目,配合下方 API 路由可看到"按仓库分别缓存"的效果;
  • SSR 安全处理:typeof window !== 'undefined'判断避免在服务端渲染时访问window(此时id为空字符串,请求路径为/api/data?id=)。

4.3 背后的运行时行为:从 useSWRHandler 看数据流

类型标注的运行时载体,是 src/index/use-swr.ts 中useSWRHandler的核心流程:_key首先经过serialize得到序列化后的key与传给 fetcher 的fnArg,然后基于 cache 订阅状态,按revalidateOnMount、revalidateIfStale等配置决定是否立即发起请求(src/index/use-swr.ts)。

实际发起请求时,fetcher 被以currentFetcher(fnArg)的方式调用,其结果与时间戳一起存入全局FETCH表,用于请求去重(src/index/use-swr.ts)。同一个页面中多个组件对同一 Key 调用useSWR时只会触发一次网络请求——这是类型安全之外,SWR 带来的另一个核心收益(自动去重与缓存共享)。

五、配套 API 路由:模拟慢速接口

pages/api/data.ts 是 Next.js API 路由,负责提供两个接口:

import { NextApiRequest, NextApiResponse } from 'next' const projects = [ 'facebook/flipper', 'vuejs/vuepress', 'rust-lang/rust', 'vercel/next.js' ] export default function api(req: NextApiRequest, res: NextApiResponse) { if (req.query.id) { // a slow endpoint for getting repo data fetch(`https://api.github.com/repos/${req.query.id}`) .then(resp => resp.json()) .then(data => { setTimeout(() => { res.json(data) }, 2000) }) return } setTimeout(() => { res.json(projects) }, 2000) }

设计意图非常清晰:

  1. 无id参数:返回写死的项目名数组projects,对应首页useSWR<string[]>的数据源;
  2. 带id参数:作为服务端代理,转发请求到 GitHub 的https://api.github.com/repos/${req.query.id},把响应原样回传给浏览器,对应动态路由页的对象数据源;
  3. 两处setTimeout(..., 2000):刻意制造 2 秒延迟,模拟真实网络环境。这正好用来观察 SWR 的加载态表现——页面代码中的'loading...'分支在请求完成前会一直渲染,2 秒后数据到达并触发 UI 更新。

提示:该 API 路由直接依赖外部的 GitHub API。在无网络环境中,服务端代理请求会失败;此时可自行将/api/data的响应替换为本地 mock 数据,不影响前端类型化演示的效果。

六、从 basic 到 basic-typescript:类型化前后的差异

将 examples/basic/libs/fetch.js 与 examples/basic-typescript/libs/fetch.ts 对照,可以看到类型化带来的三处差异:

维度纯 JS 版TS 版
fetcher 参数隐式...args,任意类型input: RequestInfo、init?: RequestInit,DOM 标准类型
返回值无约束Promise<JSON>,泛型可推导
useSWR 调用useSWR('/api/data', fetch),data为隐式anyuseSWR<string[]>(...),data.map中的元素为string

这正是 SWR 官方刻意维持的一对镜像示例:运行时行为零差异,开发期体验天壤之别——TS 版让所有数据访问点都处于编译器监督之下,接口结构调整时会立刻在 IDE 中暴露类型错误,而不是等到运行时才出错。

七、延伸实践:把类型安全用到更大的工程里

在掌握示例之后,可以按以下路线将其推广到真实项目:

  1. 抽取接口类型:将内联对象类型(如{ forks_count: number })提取为interface或type,配合fetcher<JSON>的泛型,做到"接口返回结构一处定义、多处复用";
  2. 复用同一 fetcher:libs/fetch.ts的 fetcher 不绑定任何具体 API,可在全项目共享;换用 axios 时只需保持同样的泛型签名fetchData<T>(...args): Promise<T>,页面代码无需改动;
  3. 合理利用条件请求:useSWR(null, fetch)写法适合"依赖登录态、表单输入等条件才请求"的场景,配合Fetcher条件类型可在类型层面阻止对空 Key 的误调用;
  4. 关注 strict 配置:保持tsconfig.json的strict: true,让泛型标注真正生效;若项目中存在历史 JS 文件,allowJs: true允许渐进式迁移。

八、小结

本示例虽然规模极小,却完整覆盖了"TypeScript × SWR × Next.js"三者结合的最小闭环:

  • fetcher 层:用泛型Promise<JSON>封装fetch().json(),作为类型入口;
  • 调用层:useSWR<Data>显式声明响应类型,使数据消费点全部类型安全;
  • 源码支撑:SWR 的Fetcher条件类型(src/_internal/types.ts)与useSWRHandler泛型签名(src/index/use-swr.ts)共同保证了"Key 决定 fetcher 参数、fetcher 返回类型决定 data 类型"的完整推导链路;
  • 运行验证:yarn dev启动后访问首页与任一[user]/[repo]路由,即可在 2 秒延迟的接口下观察加载态与类型化数据渲染的完整效果。

这条"类型安全的数据获取链路",就是 SWR 官方为 TypeScript 用户准备的入门台阶,也是将 SWR 引入大型工程时最值得先落地的一层基础设施。

  • 前端
  • 缓存

【免费下载链接】swr

React Hooks for Data Fetching

项目地址:https://gitcode.com/gh_mirrors/sw/swr
点击查看免费下载

相关推荐

上一篇:coinbasepro-python完整指南:10分钟掌握Coinbase Pro API交易
下一篇:Cat-Catch 2.5.9版本发布:浏览器资源嗅探的智能升级与专业级下载体验

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 6:36:58

麒麟V10信创环境下SVN服务部署与等保合规实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 6:35:03

Windows 上用 Kimi Code 搭配 ESP-IDF 搭建 ESP32-C3 开发环境并点亮 LED

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 6:33:24

纯Verilog实现FPGA硬解PNG:DEFLATE与Huffman解码实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华