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) 是维护型项目的杀手锏,路由增删只改一处
相关推荐
福兮说5 小时前
设计稿是 #4A7C6F,页面量出来是 #4B7C6F:HEX、HSL、透明度、canvas 来回转的七个坑
前端·javascript·css·canvas
郑州光合科技余经理6 小时前
海外版外卖加盟:总站与分站配送规则怎么分开管
java·开发语言·前端·后端·uni-app·php·ai编程
小呆呆6666 小时前
副业搞起来,小说,漫画,漫剧的成本优化思路
前端·后端·面试
Dovis(誓平步青云)7 小时前
浇水提醒刚弹出又消失,植物状态别只存一个百分比
开发语言·前端·javascript·pdf·ecmascript·电脑
码艺-Alimjan7 小时前
Vben Admin 新增维吾尔语 Vben-Modal的关键坑之一
前端·javascript·vue.js
可乐鸡翅yeah_8 小时前
hls.js 手动自定义 http 请求 loader,修改请求头实战
开发语言·前端·javascript·网络协议·http·ecmascript·m3u8在线
IT_陈寒8 小时前
Vite静态资源导入这个坑我帮你们踩过了
前端·人工智能·后端
广州华水科技8 小时前
大坝安全监测解决方案:单北斗GNSS形变监测系统应用与维护
前端
yivifu9 小时前
中文古籍电子书注释集成
前端·javascript·python·beautifulsoup·epub
默_笙10 小时前
🛴 从散件到整机:DeepAgents 与 Agent 身上预留的那些"插槽"(前置介绍)
前端·javascript