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) 是维护型项目的杀手锏,路由增删只改一处
相关推荐
涛涛ing5 分钟前
模型吐出半截JSON,前端如何边收边渲染?2026年AI应用必须闯过的工程关
前端
aixingpan7 分钟前
aixingpan.cnAPI开发文档:api_docs_trichart_natal_solar_return_transit2接口指南
前端·php
FRing0323 分钟前
JavaScript入门到精通二十八(forEach遍历、创建对象的三种方式、构造函数、new实例化执行过程、实例成员和静态成员)
开发语言·前端·javascript
燕落南枝37 分钟前
CSS 表单选择器与伪元素选择器详解
前端·css
拖孩38 分钟前
一个人 + AI 做的小程序,今天,正式回本了
前端·后端·微信小程序
进击的丸子44 分钟前
APP 人脸识别增值版 SDK:Camera2 无缝接入指南
前端·前端框架·代码规范
开开心心就好1 小时前
本地文件搜索工具,双击Ctrl就能呼出搜索
java·服务器·前端·支持向量机·ffmpeg·音视频·模拟退火算法
知了清语1 小时前
SVG 缩放预览:F12 无法定位子元素 & 放大模糊
前端
AN1 小时前
一个人,带着一群 AI:一个 10 年前端如何做出几十个团队在用的在线文档
前端
ByteCoder.1 小时前
班前十分钟的清单,按体检引导单来写
java·服务器·前端·助播虾