TypeScript 泛型约束的 3 层演进:从 `extends` 到条件类型到重载推导

🎯 问题场景

团队有个「通用请求封装」函数,一开始很简单:

ts 复制代码
async function request<T>(url: string): Promise<T> {
  const res = await fetch(url)
  return res.json()
}

用着用着需求变了:

  • 约定 1 :删除接口的 DELETE 请求不需要 body
  • 约定 2 :更新接口的 PUT 请求 body 必须有 id
  • 约定 3 :查询接口的 GET 请求自动带上 token

如果全写 any,CR 肯定过不了。怎么用类型系统约束住这些规则?


🔍 原因分析

问题本质:泛型的自由度过高,无法表达业务约束

  • 裸泛型 <T> 什么都能接,也什么都检查不了
  • any 就是投降,放弃类型安全
  • 我们需要逐层收敛:约束类型 → 变换类型 → 推导类型

💡 解决方案

Level 1:extends 泛型约束 --- 把门框做出来

ts 复制代码
// 至少确保 T 是个对象,排除 string/number 乱入
async function request<T extends Record<string, unknown>>(url: string, body: T): Promise<T> {
  // ...
}

// ✅ 正确
request('/api/user', { name: '张三', age: 25 })

// ❌ 类型错误:Argument of type 'string' is not assignable
request('/api/user', 'hello')

局限性:只能约束「形状」,不能约束「字段间关系」。


Level 2:条件类型 + never 过滤 --- 精细到字段级

业务场景:PUT 请求的 body 必须包含 id 字段。

ts 复制代码
// 工具类型:检测 T 是否包含 id 字段
type HasId<T> = T extends { id: string | number } ? T : never

async function putRequest<T>(url: string, body: T & HasId<T>): Promise<T> {
  return fetch(url, { method: 'PUT', body: JSON.stringify(body) }).then(r => r.json())
}

// ✅ 正确
putRequest('/api/user/1', { id: '1', name: '新名字' })

// ❌ 类型报错:Type 'string' is not assignable to type 'never'
putRequest('/api/user/1', { name: '没有id' })

妙用 :条件类型遇到不满足的约束会产出 never,而 T & never = never,直接编译报错。


Level 3:函数重载 + 泛型推导 --- 根据返回值反向推断

最进阶的用法:根据传入参数推导出精确的返回类型

ts 复制代码
// 响应体类型映射
interface ApiMap {
  '/api/user': { id: string; name: string; email: string }
  '/api/posts': { list: Array<{ id: string; title: string }>; total: number }
  '/api/config': Record<string, string>
}

// 重载 1:已知路由 → 推导响应类型
async function apiGet<T extends keyof ApiMap>(url: T): Promise<ApiMap[T]>

// 重载 2:未知路由 → 需要显式泛型
async function apiGet<T>(url: string): Promise<T>

// 实现签名
async function apiGet(url: string): Promise<unknown> {
  const res = await fetch(url)
  return res.json()
}

// 使用:
const user = await apiGet('/api/user')
//    ^? const user: { id: string; name: string; email: string }

const posts = await apiGet('/api/posts')
//    ^? const posts: { list: [...]; total: number }

const unknown = await apiGet('/api/other')
//    ^? const unknown: unknown ← 不在映射表中,需要显式泛型

这一层才是工业级方案:利用函数重载的匹配优先级,先匹配精确路由签名,再降级到通用签名。


三合一:完整版请求封装

ts 复制代码
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE'

// 约束:PUT 必须有 id
type WithId<T> = T & { id: string | number }

async function api<T extends Record<string, unknown>>(
  method: 'GET' | 'DELETE',
  url: string,
  body?: never
): Promise<T>

async function api<T extends Record<string, unknown>>(
  method: 'PUT',
  url: string,
  body: WithId<T>
): Promise<T>

async function api<T extends Record<string, unknown>>(
  method: 'POST',
  url: string,
  body: T
): Promise<T>

// 实现
async function api<T>(
  method: string,
  url: string,
  body?: unknown
): Promise<T> {
  const options: RequestInit = { method }
  if (body && method !== 'GET' && method !== 'DELETE') {
    options.body = JSON.stringify(body)
  }
  return fetch(url, options).then(r => r.json())
}

// ✅ 编译通过
api('GET', '/api/user')
api('POST', '/api/user', { name: '张三' })
api('PUT', '/api/user/1', { id: '1', name: '新名字' })

// ❌ 编译报错
api('PUT', '/api/user/1', { name: '没写id' })  // 缺少 id
api('DELETE', '/api/user/1', { something: 1 })  // DELETE 不应有 body

📌 要点总结

层级 技术 解决什么问题
Level 1 extends 泛型约束 类型的大类过滤
Level 2 条件类型 + never 字段级约束关系
Level 3 函数重载 + 映射类型 参数推导返回值
  • 不要把 any 当万能药,泛型约束只多几行代码,却能在编译期拦截大量 bug
  • 重载签名 > 联合类型参数:重载让每个签名的泛型推导独立,联合类型参数会丢失精度
  • 映射类型(ApiMap 是维护型项目的杀手锏,路由增删只改一处
相关推荐
zzzzzz3101 小时前
react-bits:从 36K Stars 的组件库,看动画交互组件该如何被评估
前端·react.js·动效
Setsuna_F_Seiei8 小时前
前端的 AI 学习之路 01 之 Agent API 调用 - 和 Agent 的基础对话
前端·人工智能·ai编程
threerocks9 小时前
AI 原生软件开发生命周期手册 - 如何借助 AI,逐阶段改造软件开发生命周期
前端·javascript·后端
徐小夕9 小时前
3分钟从想法到Agent上线:我们开源了一款AI可视化工作流“IDE”
前端·算法·github
why技术11 小时前
eli5,我觉得这个全网在吹的技能,使用体验真的很一般啊。
前端·人工智能·后端
excel11 小时前
记录升级 nuxt3 到 nuxt4 记录
前端
剑胆琴心静水深流11 小时前
全栈之路6---web集成与呈现
前端·vue.js·spring boot·分布式·spring·前端框架·npm
前端snow12 小时前
ai agent -- Memory汇总
前端
ITresearchGuest12 小时前
AI 焦虑下,前端该何去何从
前端·人工智能
Codiggerworld12 小时前
Chrome DevTools 隐藏神技:这 5 个调试技巧你可能从来没用过
前端·chrome·chrome devtools