如何为TypeScript类方法实现重试机制且保留类型安全
问题背景
我有一个用于访问Spotify数据的类,需要为其添加重试机制,因此封装了callWithRetry方法,用来调用类内的各个方法并执行重试逻辑,代码如下:
export default class SomeClass { private MAX_RETRIES = 3; // 示例默认值 // ...其他类属性 private async callWithRetry<T>(functionCall: (args: any) => Promise<T>, args: any, retries = 0): Promise<T> { try { return await functionCall(args); } catch (error) { if (retries <= this.MAX_RETRIES) { if (error && error.statusCode === 429) { const retryAfter = (parseInt(error.headers['retry-after'] as string, 10) + 1) * 1000; console.log(`sleeping for: ${retryAfter.toString()}`); await new Promise((r) => setTimeout(r, retryAfter)); } return await this.callWithRetry<T>(functionCall, args, retries + 1); } else { throw error; // 原代码笔误修正,原代码写为throw e } } }; public async getData(api: Api, id: string, offset: number = 1): Promise<string[]> { const response = await api.getStuff(id, { limit: 50, offset: offset }); if (response.statusCode !== 200) { return [] } const results = response.body.items.map(item => item.id); if (response.body.next) { const nextPage = await this.getData(api, id, offset + 1); return results.concat(nextPage); } return results } public async getOtherData(api: Api, offset: number = 0): Promise<string[]> { let response = await api.getOtherData({ limit: 50, offset: offset }); if (response.statusCode !== 200) { return [] } const results = response.body.items.map(item => item.id); if (response.body.next) { const nextPage = await this.getOtherData(api, offset + 1); return results.concat(nextPage); } return results } }
当前遇到的问题:该方法无法适配不同参数长度的函数,只能将参数以对象形式传入并标记为any类型,丢失了类型安全。原本打算为每个函数的调用签名定义接口再进行适配,有没有更合适的实现方案?
补充问题
首个方案运行效果良好,但存在新问题:传入特定类的函数时会丢失作用域,类中已初始化的属性值会丢失,后续调整后的实现代码如下:
private async executeWithRetry<T>(command: ICommand<T>, policy: RetryPolicy, api: WebApi) { while (true) { try { policy.incrementTry(); return await command.execute(api); } catch (err) { if (policy.shouldRetry(err)) { console.log("Request failed: ", command.fnName, ", retry #", policy.currentTry()) await new Promise((r) => setTimeout(r, policy.currentWait())); } else { console.log("Out of retries!!!") throw err; // 向上抛出错误 } } } } class PaginatedCommand<T> implements ICommand<T> { constructor(private fn: (options?: { limit?: number, offset?: number }) => Promise<T>, private options: { limit: number, offset: number }) {} public async execute(api: WebApi): Promise<T> { //@ts-ignore return await api[this.fn.name](this.options); } public get fnName(): string { return this.fn.name; } } class PlaylistPaginatedCommand<T> implements ICommand<T> { constructor(private fn: (Id: string, options?: { limit?: number, offset?: number }) => Promise<T>, private Id: string, private options: { limit: number, offset: number }) {} public async execute(api: WebApi): Promise<T> { //@ts-ignore return await api[this.fn.name](this.Id, this.options); } public get fnName(): string { return this.fn.name; } }
优化方案
直接基于TypeScript类型推断+闭包重写即可,不需要用命令模式做复杂适配,也不会出现作用域丢失问题,完全保留类型安全:
核心实现
export default class SomeClass { private MAX_RETRIES = 3; // 泛型直接适配任意返回值的异步函数,不需要处理参数 private async callWithRetry<T>(fn: () => Promise<T>, retries = 0): Promise<T> { try { // 直接执行闭包,参数已经提前绑定在闭包内,不需要额外传参 return await fn(); } catch (error: any) { if (retries < this.MAX_RETRIES) { // 原代码<=会多执行一次重试,此处修正为总共重试MAX_RETRIES次 if (error?.statusCode === 429) { const retryAfter = (parseInt(error.headers?.['retry-after'] || '1', 10) + 1) * 1000; console.log(`触发限流,等待 ${retryAfter}ms 后重试`); await new Promise(r => setTimeout(r, retryAfter)); } return this.callWithRetry(fn, retries + 1); } console.error(`重试${this.MAX_RETRIES}次后仍然失败`); throw error; } } public async getData(api: Api, id: string, offset: number = 1): Promise<string[]> { // 调用的时候直接把执行逻辑包成箭头函数闭包,参数、作用域都完全保留,类型自动推断 const response = await this.callWithRetry(() => api.getStuff(id, { limit: 50, offset: offset })); if (response.statusCode !== 200) { return [] } const results = response.body.items.map(item => item.id); if (response.body.next) { const nextPage = await this.getData(api, id, offset + 1); return results.concat(nextPage); } return results } public async getOtherData(api: Api, offset: number = 0): Promise<string[]> { // 任意参数数量的函数都可以直接包成闭包传入,不需要调整重试方法 const response = await this.callWithRetry(() => api.getOtherData({ limit: 50, offset: offset })); if (response.statusCode !== 200) { return [] } const results = response.body.items.map(item => item.id); if (response.body.next) { const nextPage = await this.getOtherData(api, offset + 1); return results.concat(nextPage); } return results } }
方案优势
- 完全类型安全:无需额外定义接口,也不需要使用
any类型,传入闭包的返回值会被自动推导,重试方法的返回值和业务方法完全对齐 - 不存在作用域丢失问题:箭头函数闭包会自动捕获当前上下文的所有依赖,包括api实例、参数、类属性等,不会出现属性丢失的情况
- 全场景适配:无论被调用的方法有多少个参数,只需要把执行逻辑包成无参异步闭包传入即可,不需要修改重试方法的代码
- 代码更简洁:不需要定义多套命令类,不需要通过函数名反射调用方法,也不需要加
@ts-ignore忽略类型检查
内容的提问来源于stack exchange,提问作者Komi
相关产品推荐
相关产品推荐

