登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

TypeScript 自定义类型守卫收窄 API 响应

来源:17golang原创

时间:2026-10-17 09:30:52 493浏览 收藏

API 返回 JSON,并不等于返回了符合 TypeScript 接口的数据。更稳妥的做法是先把 Response.json() 的结果视为 unknown,在运行时检查对象形状,再用自定义类型守卫把它收窄成业务类型。这样既保留了编译器提示,也不会把服务端字段漂移伪装成安全数据。

官方资料:https://www.typescriptlang.org/docs/handbook/2/narrowing

要点速览
  • Response.json() 只负责解析 JSON,不负责证明字段符合接口。
  • 类型谓词必须写成 value is UserResponse,返回值为真时才会触发收窄。
  • 守卫要覆盖 null、数组、必需字段和嵌套元素,不能只靠 as 强转。

先把 API 响应当作 unknown

浏览器的 Response.json() 会读取响应体并解析成 JavaScript 值,结果可能是对象、数组、字符串或数字。接口文档里的 UserResponse 只是编译期描述,无法约束网络另一端实际发来的内容。因此入口处先保留 unknown,把信任边界放在一个可复用的守卫函数里。

interface UserResponse {
  id: string;
  name: string;
  roles: string[];
}

function isRecord(value: unknown): value is Record {
  // 排除 null 和数组,避免把非字典值当成响应对象。
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function isUserResponse(value: unknown): value is UserResponse {
  // 先验证对象,再验证业务字段和数组元素的类型。
  return isRecord(value)
    && typeof value.id === "string"
    && typeof value.name === "string"
    && Array.isArray(value.roles)
    && value.roles.every((role) => typeof role === "string");
}
TypeScript API 响应从 fetch 和 Response.json 进入 unknown,再经过 isRecord 与 isUserResponse 收窄为 UserResponse 的结构说明图
图1:API 响应边界说明图,展示解析结果、运行时守卫和业务类型之间的静态关系。

predicate 让调用点获得安全字段

isUserResponse 的关键不是函数名,而是返回类型中的 value is UserResponse。这叫类型谓词,参数名必须对应当前函数签名里的参数。若只写成 (value: unknown) => boolean,调用者只能知道结果是布尔值,不能因此直接访问 value.id。

async function loadUser(url: string): Promise {
  const response = await fetch(url);
  const raw: unknown = await response.json();

  // HTTP 成功只说明状态码正常,仍需分别处理业务结构错误。
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  if (!isUserResponse(raw)) {
    throw new Error("API 响应缺少有效的用户字段");
  }

  // 通过守卫后,raw 在这里已经被收窄为 UserResponse。
  return raw;
}

这里把 HTTP 状态判断和 JSON 结构判断分开,排查时更容易定位:状态码失败属于请求层问题,结构失败则说明服务端契约或版本兼容出现变化。不要先写 const user = raw as UserResponse,那只是关闭检查,不会增加运行时保护。

把守卫边界写清楚,避免“半校验”

检查项应确认什么遗漏后的风险
顶层值对象、非 null、非数组字符串或数组被当成记录访问
必需字段id、name 的实际类型字段缺失时页面出现空值或异常
嵌套数组roles 是数组且每项为字符串后续 map、join 的假设失效
接口演进新增字段是否可选过严校验导致兼容旧响应失败

守卫只应检查当前业务真正依赖的字段。服务端新增一个无关字段通常不需要让旧前端失败;但如果 roles 决定权限显示,就应把它作为必需字段并记录结构错误。

同一个 predicate 还能复用到数组过滤

类型守卫不仅适合 if 分支,也能传给 filter。当输入是联合数组时,返回类型会把结果收窄为 UserResponse[],比先过滤再强转更可靠。

const payloads: unknown[] = [
  { id: "u-1", name: "Lin", roles: ["editor"] },
  { id: 7, name: "invalid", roles: [] },
];

// filter 使用类型谓词,结果数组只保留通过完整校验的对象。
const users: UserResponse[] = payloads.filter(isUserResponse);
const labels = users.map((user) => `${user.name}: ${user.roles.join(", ")}`);
TypeScript 类型谓词从 isUserResponse 和 parameter is Type 连接到 if 分支、filter 结果与 UserResponse 数组的结构说明图
图2:predicate 收窄关系图,强调同一守卫在分支和数组过滤中的复用边界。

常见问题

为什么接口写对了还要校验 JSON?

接口只参与 TypeScript 编译,网络数据在运行时进入应用前没有自动经过类型检查。外部输入必须在边界处验证。

类型守卫能保证服务端永远正确吗?

不能。它只能保证当前这次输入通过了你写下的条件;复杂嵌套对象仍应逐层检查,必要时记录结构错误。

什么时候可以用类型断言?

只有在数据已经由可信校验层验证,且断言只是把已知事实传递给编译器时才适合。原始 API 响应不应直接断言成业务类型。

收窄的核心顺序可以固定为:外部输入先是 unknown,守卫负责运行时判断,谓词把判断结果传回 TypeScript,业务代码最后再访问字段。只要把这个顺序放在请求边界,API 变化就会以可定位的结构错误暴露出来,而不是在页面深处变成偶发的空值异常。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>