在 TypeScript 项目中接入 Payment Request API 时,不少开发者会直接依赖 lib.dom.d.ts 中内置的声明,但这些声明为了兼容不同浏览器版本,往往把某些字段定义得过于宽泛。支付信息涉及金额、币种、商品明细和配送选项,任何一处类型定义不严谨,都可能在测试阶段或生产环境中引发难以排查的问题。因此,建议根据实际业务需要,在项目里重新声明一套更精确的支付信息类型,并在构建支付请求前对关键字段进行校验。

金额类型为什么必须用字符串
Payment Request API 中最容易出错的是 PaymentCurrencyAmount 接口,它只有两个属性:currency 和 value。官方规范里明确要求 value 是一个十进制字符串,而不是 number 类型。这是因为 IEEE 754 双精度浮点数无法精确表示 0.1 或 0.01 这样的十进制小数,直接使用 number 计算金额可能产生 0.1 + 0.2 不等于 0.3 的结果。在支付场景中,哪怕一分钱的误差也不允许出现,所以金额必须用字符串承载,最终由后端或支付网关按最小货币单位处理。
很多第三方类型库或者手写声明会图方便,把 value 声明成 number,这虽然能让代码少做一次 toString() 转换,却埋下了精度隐患。更合理的做法是保留字符串类型,并在表单提交前通过正则校验格式,例如:
interface PaymentCurrencyAmount {
currency: string;
value: string;
}
function isValidAmount(amount: PaymentCurrencyAmount): boolean {
return /^[0-9]+(\.[0-9]{1,2})?$/.test(amount.value)
&& /^[A-Z]{3}$/.test(amount.currency);
}
上面的校验函数要求 currency 必须是三个大写字母组成的 ISO 4217 货币代码,例如 USD、CNY、EUR;value 只允许整数或者最多两位小数,避免在客户端把超出精度的数值传给支付接口。如果业务中还需要支持零金额或退款场景,可以再放宽正则规则,但类型定义仍然保持字符串。
在 TypeScript 中,这种校验函数最好返回类型守卫,这样后续代码可以安全地把普通对象收窄为合法的支付金额对象。比如可以定义一个名义类型或品牌类型,但大多数项目直接使用接口加运行时校验就已经足够。
支付方式与支付详情类型
发起支付请求需要两个核心对象:支付方式列表和支付详情。支付方式对应 PaymentMethodData 接口,它至少包含 supportedMethods 字段,用于声明支持哪种支付方式,例如 basic-card 或者某个支付网关的 URL。 data 字段是可选的,用来存放特定支付方式需要的额外参数,例如银行卡网络限制或第三方渠道的商户号。
由于不同支付渠道的 data 结构差异很大,TypeScript 中很难用一个固定接口覆盖所有情况。通常的做法是给 data 定义一个带索引签名的类型,这样既能获得基础的编译检查,又不会阻塞扩展字段。下面是一个可复用的声明:
interface PaymentMethodData {
supportedMethods: string;
data?: {
[key: string]: unknown;
};
}
interface PaymentItem {
label: string;
amount: PaymentCurrencyAmount;
pending?: boolean;
}
interface PaymentShippingOption {
id: string;
label: string;
amount: PaymentCurrencyAmount;
selected?: boolean;
}
interface PaymentDetailsInit {
total: PaymentCurrencyAmount;
id?: string;
displayItems?: PaymentItem[];
shippingOptions?: PaymentShippingOption[];
}
PaymentDetailsInit 的 total 字段是必须的,表示订单总金额;displayItems 是可选的明细列表,展示商品单价、税费、折扣等;shippingOptions 则是可选的配送方式列表。选中项由 selected 属性标记,但浏览器并不强制要求必须有默认选中项。这里把 data 的索引签名值类型定义为 unknown,比 any 更安全,因为使用前必须进行类型缩小。
如果你的项目只支持固定的支付方式,比如只支持微信支付和支付宝,可以把 supportedMethods 声明成字符串字面量联合类型,这样在传入非法方式时 TypeScript 就会直接报错。例如:
type SupportedMethod = 'https://wx.tenpay.com' | 'https://open.alipay.com';
interface FixedPaymentMethodData {
supportedMethods: SupportedMethod;
data?: {
[key: string]: unknown;
};
}
这种写法能提前拦截拼写错误,也会让 IDE 提供更好的自动补全提示。需要注意的是,浏览器对 basic-card 的支持已经逐渐减少,实际业务中更应该使用支付网关提供的专用 URL,而不是依赖基础卡支付。
请求选项与完整初始化流程
除了支付方式和支付详情,PaymentRequest 构造函数还接收第三个参数 PaymentOptions,用来声明需要用户提供哪些额外的支付信息。这个接口包含四个布尔值字段:requestPayerName、requestPayerEmail、requestPayerPhone 和 requestShipping,以及一个 shippingType 字段,取值可以是 shipping、delivery 或 pickup。
在 TypeScript 中可以直接使用内置的 PaymentOptions 类型,但如果想限制 shippingType 的取值,可以定义更严格的枚举或联合类型。下面给出一个完整的构造流程:
const supportedMethods: PaymentMethodData[] = [
{
supportedMethods: 'https://wx.tenpay.com',
data: {
merchantId: '123456'
}
}
];
const details: PaymentDetailsInit = {
total: {
label: '订单总价',
amount: {
currency: 'CNY',
value: '128.50'
}
},
displayItems: [
{
label: '商品小计',
amount: {
currency: 'CNY',
value: '120.00'
}
},
{
label: '配送费',
amount: {
currency: 'CNY',
value: '8.50'
}
}
]
};
const options: PaymentOptions = {
requestPayerName: true,
requestPayerEmail: true,
requestPayerPhone: false,
requestShipping: true,
shippingType: 'delivery'
};
if (window.PaymentRequest) {
const request = new PaymentRequest(supportedMethods, details, options);
request.canMakePayment().then((canPay) => {
if (canPay) {
// 展示支付按钮
}
});
}
这里先判断 window.PaymentRequest 是否存在,避免在不支持的浏览器中直接实例化导致异常。canMakePayment() 返回一个 Promise<boolean>,可以用来决定是否展示支付按钮。实际项目中,建议把这一串逻辑封装成独立的模块或自定义 Hook,在组件挂载时判断环境,再根据结果渲染界面。
另外,PaymentRequest 实例还提供了 show() 方法,它必须在用户手势触发的回调中调用,否则浏览器会拒绝弹出支付面板。TypeScript 类型层面并不会阻止错误的调用时机,所以运行时仍然需要遵循规范。错误处理一般放在 show() 返回的 Promise 中,捕获 AbortError、NotAllowedError 等异常。
扩展类型与常见误区
支付信息类型定义过程中,最常见的误区之一是直接复用后端返回的商品列表类型。后端接口可能返回 amount 为 number,前端如果不做转换就赋给 PaymentItem,会破坏类型约束。正确做法是在 API 响应层做一次映射,把数字金额乘以 100 后取整,再格式化成字符串。这个转换不要放在组件渲染逻辑里,而应该集中在数据请求层,保证进入支付流程的数据永远符合 PaymentCurrencyAmount 的约定。
另一个容易混淆的地方是 PaymentDetailsUpdate。当用户选择不同的配送地址后,浏览器会触发 shippingaddresschange 事件,需要返回一个新的 PaymentDetailsUpdate 对象来更新总价和明细。这个类型与 PaymentDetailsInit 类似,但多了 error 字段,用来向用户显示错误信息。为了让类型更清晰,可以声明为:
interface PaymentDetailsUpdate extends PaymentDetailsInit {
error?: string;
}
function handleShippingAddressChange(
event: Event
): PaymentDetailsUpdate | undefined {
const target = event.target as PaymentRequest;
const shippingOption = target.shippingOption;
// 根据地址重新计算运费
return {
total: {
label: '订单总价',
amount: {
currency: 'CNY',
value: '135.00'
}
}
};
}
上面的 handleShippingAddressChange 只是演示类型接口,并没有真正读取地址对象。浏览器规范中,shippingaddresschange 事件的 target 是 PaymentRequest 实例,可以通过 shippingAddress 属性获取收货地址。在 TypeScript 中,需要手动断言 event.target 为 PaymentRequest,因为标准事件监听器的泛型默认是 EventTarget。
最后要提醒的是,不要为了少写几个类型声明就滥用 as any。一旦支付参数被断言成 any,TypeScript 就失去了拦截非法字段的作用,后续重构时很容易遗漏某个金额字段的类型变更。保持支付信息相关接口集中在一个文件中维护,并导出给前端各业务模块使用,能在长期项目中显著降低维护成本。
TypeScriptPayment Request API支付信息类型修改时间:2026-09-18 01:01:32