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();
关键修改说明
- 替换
any为unknown:unknown是TypeScript的安全顶级类型,它代表任意值但禁止无类型断言的操作,避免了any带来的类型不安全问题。 - 移除不必要的类型断言:
QueueMember<T>可以自动赋值给QueueMember<unknown>(因为T是unknown的子类型),所以无需as QueueMember<T>断言。 - 保留全流程泛型:
inQueue方法返回的Promise<T>仍然严格对应请求函数的返回值类型,外部调用时能正确推断类型(比如requestProfile返回Promise<string>,队列调用后仍返回Promise<string>)。
为什么这个方案可行
队列中的每个任务虽然泛型类型不同,但它们都符合QueueMember<unknown>的结构,而我们只需要将请求结果传递给对应的resolve/reject,不需要直接操作结果值,因此用unknown完全满足需求,同时保证了类型安全。
内容的提问来源于stack exchange,提问作者CatCatcher
相关产品推荐
相关产品推荐

