You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 22:24:02