实战:写一个类型安全的 API 客户端
本节目标
- 把前面学的接口、泛型、判别联合串起来
- 写一个「请求有类型、响应有类型、错误也能区分」的 API 客户端
- 体会 TS 在真实业务里的价值
我们用泛型 + 判别联合做一个「类型安全」的 fetch 封装:
// 运行环境:Node.js 18+ + typescript;类型检查 npx tsc --noEmit ts-l15.ts
// 说明:下面代码用浏览器/Node18 的 fetch;运行时直接跑需网络,类型部分不依赖网络
// 统一的响应结构(判别联合:用 ok 字段区分成功/失败)
type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; error: string }
// 通用请求函数:传入「响应数据类型 T」,返回对应 ApiResult<T>
async function getJSON<T>(url: string): Promise<ApiResult<T>> {
try {
const res = await fetch(url)
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` }
const data = (await res.json()) as T // 这里 as 是必要的:网络数据是 unknown
return { ok: true, data }
} catch (e) {
return { ok: false, error: String(e) }
}
}
// 使用示例:定义接口返回结构
interface User { id: number; name: string }
async function main() {
const r = await getJSON<User>('https://api.example.com/user/1')
if (r.ok) {
console.log(r.data.name) // 这里 data 是 User,有提示
} else {
console.error(r.error) // 这里走错误分支
}
}注意:网络返回的是「运行时数据」,TS 只能「猜」成 T(用 as),真正的校验还需 zod 这类运行时库。但至少调用方拿到了整齐的 r.ok ? data : error 分支,不会漏处理错误。
名词解释
判别联合实战(Result 类型):用 { ok: true; data } | { ok: false; error } 这种结构,把「成功/失败」编码进类型。调用方必须 if (r.ok) 才能拿到 data,于是「忘了处理错误」在编译期就不可能发生。这是 Rust 的 Result、Go 的 (值, err) 思想在 TS 里的落地。
泛型封装(Generic Wrapper):把「变化的部分」(这里是响应数据类型 T)抽成泛型参数,于是 getJSON<User>、getJSON<Order> 复用同一套逻辑却各自保留精确类型。这是泛型最经典的实战价值。
课后练习
练习 1:如果 getJSON 不返回判别联合,而直接 return data,会有什么隐患?
答案:调用方拿到的「可能是数据也可能是 undefined/抛错」,TS 无法强制你判空/判错,很容易写出
data.name在失败时崩的代码。判别联合强制if (r.ok)才能用data,把错误处理变成「编译期义务」。
练习 2:为什么网络 JSON 仍需 as T 或运行时校验,TS 不能直接保证?
答案:因为接口返回的是运行时字符串,TS 编译期不知道它真实结构。TS 只能「按你声明的 T 去理解」,但「声明是否和真实数据一致」需要
zod等运行时校验来保证。as是信任声明,zod是验证声明。
总结
这一节是前面所有知识的「阅兵式」。我的核心观点:TS 在业务里最大的价值,不是「写类型时爽」,而是「逼你把错误的可能性和数据的形状都显式建模出来」。ApiResult<T> 这种判别联合,把「网络可能失败」从「藏在心里的事」变成「编译器盯着你的事」。新手常觉得「包一层 Result 好啰嗦」,但当你的接口从 1 个涨到 100 个、线上事故从「偶发白屏」变成「编译期就拦下」,你会感谢这层啰嗦。顺便强调:as T 只是信任,真要稳请用 zod 在边界做运行时校验——TS 管编译期,zod 管边界,各司其职。