页面类型检查通过,上线后却出现“价格是字符串”或“库存字段消失”。常见原因是把接口响应直接断言成某个类型。类型声明描述的是代码中的约定,外部服务、旧缓存和测试替身仍可能返回不符合约定的数据。让 Claude 帮忙时,应该把任务放在系统边界:收到什么、接受什么、拒绝什么,以及失败后界面怎样恢复。
一 先给出成功与失败样本
下面假设商品接口的有效响应是 {"id":"p1","priceCents":1990,"inStock":true}。金额使用非负整数分,商品编号不能为空,库存状态必须是布尔值。未知字段允许出现,但不会被复制到返回值。故意提供 priceCents: "1990"、inStock: "false" 和 null,防止 Claude 用 Number() 或 Boolean() 把坏数据悄悄变成看似正确的值。
如果你使用 TypeScript,入口可声明为 unknown,在检查后缩小类型;TypeScript narrowing 文档解释了这种检查与类型收窄的关系。本文使用普通 JavaScript,便于独立运行。
二 建一个严格但有限的边界函数
保存为 product.mjs,运行 node product.mjs:
import assert from 'node:assert/strict';
function parseProduct(value) {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('商品必须是对象');
}
if (typeof value.id !== 'string' || value.id.trim() === '') {
throw new TypeError('id 必须是非空字符串');
}
if (!Number.isSafeInteger(value.priceCents) || value.priceCents < 0) {
throw new TypeError('priceCents 必须是非负整数分');
}
if (typeof value.inStock !== 'boolean') {
throw new TypeError('inStock 必须是布尔值');
}
return { id: value.id, priceCents: value.priceCents, inStock: value.inStock };
}
async function loadProduct(url, fetcher = fetch) {
const response = await fetcher(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return parseProduct(await response.json());
}
const good = { id: 'p1', priceCents: 1990, inStock: true };
assert.deepEqual(parseProduct({ ...good, note: 'ignored' }), good);
for (const bad of [null, [], { ...good, priceCents: '1990' },
{ ...good, inStock: 'false' }]) {
assert.throws(() => parseProduct(bad));
}
assert.deepEqual(await loadProduct('/products/p1', async () => ({
ok: true, json: async () => good
})), good);
await assert.rejects(() => loadProduct('/products/p1', async () => ({
ok: false, status: 503
})), /HTTP 503/);
console.log('product boundary checks passed');
fetcher 参数让测试能够替换网络,而不需要真实请求。它并不模拟浏览器的全部行为。MDN 的 Fetch 使用说明提醒:HTTP 错误状态本身不一定让 fetch() 拒绝,因此需要检查 response.ok。
三 可复制提示词
请审查我的商品接口边界。成功结构:id 为非空字符串,priceCents 为非负安全整数, inStock 为布尔值;未知字段忽略。这里是脱敏响应样本和现有读取函数:[粘贴]。 请分别处理 HTTP 非成功、JSON 解析失败、字段不合法,不要自动把字符串转成数字或布尔值。 给出不新增依赖的最小修复,以及可注入 fetch 替身的测试。 输出接口假设、完整代码、验证命令、仍未覆盖的风险;不要把未执行测试写成通过。
如果字段允许缺省,要进一步说明缺省的业务含义。例如缺少库存字段是否代表“未知”,并不自动等于“无货”。让模型列出疑问,再由你决定规则。
四 接入与验收
在页面入口捕获错误,展示可理解的失败状态,并提供适合该页面的重试入口。不要把原始响应、令牌或用户数据直接打印给终端用户。请求超时、认证和重试策略属于下一层设计,不能靠字段校验解决。
本文示例已在 Node.js 24.19.0 中执行;验证覆盖正常响应、类型错误、额外字段和模拟的 503 状态,没有访问生产接口。项目验收还应补非法 JSON、超大响应和字段版本变更。一个边界函数的价值,是把不可信数据挡在业务计算之前,并让故障能被定位到明确的一层。
五 错误分类要帮助定位
HTTP 503、无法解析 JSON 和价格字段类型错误,可能来自不同故障。业务层可以把它们映射成不同的内部错误类别,同时向用户显示简短提示。不要把所有失败都改成空商品对象,因为后面的价格计算会失去“数据根本没有加载成功”这一事实。必要时记录非敏感的错误类别和字段名,而不是完整响应。
当接口允许扩展字段时,白名单返回可以降低无意传播额外信息的机会;当协议要求严格一致时,则应把未知字段也视为不兼容。两种做法都可能合理,需要由调用方契约决定。若项目已经采用成熟的运行时校验库,让 Claude 复用现有方案,避免维护两套字段规则。无论使用哪个库,都要保留字符串金额与字符串布尔值这类反例。
方法参考:Anthropic 提示工程概览。