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

TypeScript实现异步请求队列时如何保留泛型类型

解决TypeScript请求并发控制队列的泛型丢失问题

问题背景

编写前端请求并发控制类AsyncLimitQueue时,等待队列#waitingQueue使用QueueMember<any>导致泛型类型丢失,无法在全流程保留请求方法的返回值类型,希望队列能支持不同返回类型的请求函数,同时保证类型安全。

原简化代码如下:

async function requestProfile(uid: string): Promise<string> {
  // Simulate a user profile request
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(`UserProfile-${uid}`);
    }, 1000);
  });
}

// A more elegant implementation
type QueueMember<T> = {
  toRequest: () => Promise<T>;
  resolve: (value: T | PromiseLike<T>) => void;
  reject: (reason?: any) => void;
};
class AsyncLimitQueue {
  #limit = 7;
  #waitingQueue: Array<QueueMember<any>> = [];

  constructor(limit?: number) {
    this.#limit = limit ?? this.#limit;
  }

  #execWaitingQueue() {
    if (this.#limit === 0 || this.#waitingQueue.length === 0) return;

    // Todo--toRequest, resolve lose generic type <T>
    const { toRequest, resolve, reject } = this.#waitingQueue.shift()!;
    this.#limit--; // Decrement the semaphore
    toRequest()
      .then(resolve)
      .catch(reject)
      .finally(() => {
        this.#limit++; // Increment the semaphore
        this.#execWaitingQueue(); // Try executing requests in the waiting queue
      });
  }

  inQueue<T>(toRequest: () => Promise<T>): Promise<T> {
    return new Promise<T>((resolve, reject) => {
      // All requests enter the queue first
      this.#waitingQueue.push({
        toRequest,
        resolve,
        reject,
      } as QueueMember<T>);
      // Attempt to execute the waiting queue
      this.#execWaitingQueue();
    });
  }
}

async function loadUserProfiles() {
  const start = new Date().getTime();
  const uids: string[] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"];
  const queue = new AsyncLimitQueue(5);
  const profiles = await Promise.all(uids.map((id) => queue.inQueue(() => requestProfile(id))));
  console.log(new Date().getTime() - start > 2000 && new Date().getTime() - start < 3000, profiles);
}

loadUserProfiles();

解决方案

核心是用unknown替代any作为队列成员的泛型类型,同时利用TypeScript的类型兼容性实现类型安全,无需手动断言。

修改后的完整代码

async function requestProfile(uid: string): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(`UserProfile-${uid}`);
    }, 1000);
  });
}

type QueueMember<T> = {
  toRequest: () => Promise<T>;
  resolve: (value: T | PromiseLike<T>) => void;
  reject: (reason?: any) => void;
};

class AsyncLimitQueue {
  #limit = 7;
  // 用unknown替代any,保证类型安全
  #waitingQueue: Array<QueueMember<unknown>> = [];

  constructor(limit?: number) {
    this.#limit = limit ?? this.#limit;
  }

  #execWaitingQueue() {
    if (this.#limit === 0 || this.#waitingQueue.length === 0) return;

    const { toRequest, resolve, reject } = this.#waitingQueue.shift()!;
    this.#limit--;
    toRequest()
      .then(resolve)
      .catch(reject)
      .finally(() => {
        this.#limit++;
        this.#execWaitingQueue();
      });
  }

  inQueue<T>(toRequest: () => Promise<T>): Promise<T> {
    return new Promise<T>((resolve, reject) => {
      // 无需类型断言,QueueMember<T>自动兼容QueueMember<unknown>
      this.#waitingQueue.push({
        toRequest,
        resolve,
        reject,
      });
      this.#execWaitingQueue();
    });
  }
}

async function loadUserProfiles() {
  const start = new Date().getTime();
  const uids: string[] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"];
  const queue = new AsyncLimitQueue(5);
  const profiles = await Promise.all(uids.map((id) => queue.inQueue(() => requestProfile(id))));
  console.log(new Date().getTime() - start > 2000 && new Date().getTime() - start < 3000, profiles);
}

loadUserProfiles();

关键修改说明

  1. 替换any为unknown:unknown是TypeScript的安全顶级类型,它代表任意值但禁止无类型断言的操作,避免了any带来的类型不安全问题。
  2. 移除不必要的类型断言:QueueMember<T>可以自动赋值给QueueMember<unknown>(因为T是unknown的子类型),所以无需as QueueMember<T>断言。
  3. 保留全流程泛型:inQueue方法返回的Promise<T>仍然严格对应请求函数的返回值类型,外部调用时能正确推断类型(比如requestProfile返回Promise<string>,队列调用后仍返回Promise<string>)。

为什么这个方案可行

队列中的每个任务虽然泛型类型不同,但它们都符合QueueMember<unknown>的结构,而我们只需要将请求结果传递给对应的resolve/reject,不需要直接操作结果值,因此用unknown完全满足需求,同时保证了类型安全。

内容的提问来源于stack exchange,提问作者CatCatcher

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 17:24:57