TypeScript Promise.allSettled类型演进
理解 Promise.allSettled
基础概念
在 JavaScript 中,Promise.allSettled
是一个用于处理多个 Promise 对象的方法。它会返回一个新的 Promise,当所有传入的 Promise 都已 敲定(settled,即已完成(resolved)或已拒绝(rejected))时,这个新的 Promise 才会完成。
来看一个简单的 JavaScript 示例:
const promises = [
Promise.resolve(1),
Promise.reject('error'),
Promise.resolve(3)
];
Promise.allSettled(promises).then(results => {
console.log(results);
});
上述代码中,Promise.allSettled
接受一个 Promise 数组 promises
。无论每个 Promise 是成功还是失败,Promise.allSettled
返回的 Promise 都会成功,并将包含每个 Promise 结果的数组作为参数传递给 .then
回调。在这个例子中,results
数组会包含三个对象,分别对应三个传入的 Promise 的状态和结果。
在 TypeScript 中,对 Promise.allSettled
的类型定义有着重要的作用,它能让开发者在编写代码时获得更准确的类型提示,从而提高代码的健壮性和可维护性。
TypeScript 早期对 Promise.allSettled
的类型定义
在早期的 TypeScript 版本中,Promise.allSettled
的类型定义相对简单。它的类型声明大致如下:
declare function allSettled<T>(promises: Array<PromiseLike<T>>): Promise<Array<{
status: 'fulfilled';
value: T;
} | {
status:'rejected';
reason: any;
}>>;
这里,allSettled
接受一个 PromiseLike<T>
类型的数组 promises
,返回一个 Promise
。这个返回的 Promise
成功时,其值是一个数组,数组中的每个元素是一个对象,该对象有两种可能的结构:
- 如果
status
是'fulfilled'
,则包含value
属性,其类型为T
,表示 Promise 成功时的值。 - 如果
status
是'rejected'
,则包含reason
属性,类型为any
,表示 Promise 失败时的原因。
来看一个使用示例:
const promises: Promise<number>[] = [
Promise.resolve(1),
Promise.reject('error'),
Promise.resolve(3)
];
Promise.allSettled(promises).then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Fulfilled value:', result.value);
} else {
console.log('Rejected reason:', result.reason);
}
});
});
在这个示例中,promises
数组中的每个 Promise 都被定义为 Promise<number>
类型。当使用 Promise.allSettled
处理这些 Promise 时,results
数组中的每个元素的类型符合上述定义。然而,这里有一个明显的问题,就是 reason
的类型被定义为 any
,这意味着在处理拒绝原因时,TypeScript 无法提供准确的类型检查,开发者可能会在运行时遇到类型相关的错误。
类型演进需求分析
随着代码库的增长和对类型安全性要求的提高,reason
类型为 any
的问题变得愈发突出。例如,在大型项目中,不同的 Promise 可能因为不同的业务逻辑而拒绝,这些拒绝原因可能有着不同的类型。如果统一使用 any
类型,就无法在编译时发现对拒绝原因处理不当的问题。
假设我们有一个函数,它从 API 获取用户信息,可能因为网络问题、权限问题等原因拒绝 Promise:
function getUserInfo(): Promise<User> {
// 模拟异步操作
return new Promise((resolve, reject) => {
// 这里可能因为各种原因拒绝
reject(new Error('Network error'));
});
}
Promise.allSettled([getUserInfo()]).then(results => {
if (results[0].status ==='rejected') {
// 这里 `results[0].reason` 类型为 `any`,但实际应该是 `Error` 类型
console.log(results[0].reason.message);
}
});
在这个例子中,getUserInfo
函数的 Promise 拒绝原因是 Error
类型,但由于 Promise.allSettled
类型定义中 reason
是 any
,TypeScript 无法检查 results[0].reason.message
是否正确,因为 any
类型可以有任何属性,这可能导致运行时错误。
因此,需要对 Promise.allSettled
的类型定义进行演进,以提供更准确的类型信息,特别是对于拒绝原因的类型。
改进后的 Promise.allSettled
类型定义
为了解决 reason
类型不准确的问题,TypeScript 后来对 Promise.allSettled
的类型定义进行了改进。改进后的类型声明如下:
declare function allSettled<T>(promises: readonly (PromiseLike<T> | T)[]): Promise<{
status: 'fulfilled';
value: T;
} | {
status:'rejected';
reason: any;
}[];
declare function allSettled<T extends readonly (PromiseLike<unknown> | unknown)[]>(promises: T): Promise<{
[K in keyof T]: T[K] extends PromiseLike<infer U>? {
status: 'fulfilled';
value: U;
} : {
status: 'fulfilled';
value: T[K];
} | {
status:'rejected';
reason: any;
};
}>;
这里使用了泛型和条件类型来使类型定义更加灵活和准确。主要有以下几点改进:
- 泛型数组类型:
promises
参数的类型从Array<PromiseLike<T>>
变为readonly (PromiseLike<T> | T)[]
,这意味着不仅可以传入PromiseLike<T>
类型的数组,还可以传入包含直接值(非 Promise)的数组,并且该数组是只读的,防止意外修改。 - 条件类型处理:第二个重载使用了条件类型和映射类型。对于
promises
数组中的每个元素T[K]
,如果它是PromiseLike<infer U>
类型(即 Promise 类型),则根据其状态返回相应的{ status: 'fulfilled'; value: U; }
或{ status:'rejected'; reason: any; }
;如果不是 Promise 类型,则返回{ status: 'fulfilled'; value: T[K]; }
。
来看一个改进后的使用示例:
type User = { name: string; age: number };
function getUserInfo(): Promise<User> {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('User not found'));
}, 1000);
});
}
function getDefaultUser(): User {
return { name: 'default', age: 0 };
}
const promises = [getUserInfo(), getDefaultUser()];
Promise.allSettled(promises).then(results => {
results.forEach((result, index) => {
if (index === 0) {
if (result.status ==='rejected') {
// 这里 `result.reason` 类型更准确,虽然仍然是 `any`,但可改进
console.log((result.reason as Error).message);
}
} else {
console.log('Default user:', result.value);
}
});
});
在这个示例中,promises
数组包含一个 Promise(getUserInfo
)和一个直接值(getDefaultUser
)。Promise.allSettled
返回的 results
数组中的元素类型根据条件类型得到了更准确的定义。然而,reason
类型仍然是 any
,我们还可以进一步改进。
进一步细化 reason
类型
为了进一步细化 reason
类型,我们可以通过自定义类型来表示不同的拒绝原因。假设我们有不同的业务逻辑导致 Promise 拒绝,我们可以定义如下类型:
type NetworkError = { type: 'network'; message: string };
type PermissionError = { type: 'permission'; message: string };
function fetchData(): Promise<string> {
return new Promise((resolve, reject) => {
// 模拟网络错误
reject({ type: 'network', message: 'Network connection lost' } as NetworkError);
});
}
function checkPermission(): Promise<void> {
return new Promise((resolve, reject) => {
// 模拟权限错误
reject({ type: 'permission', message: 'Access denied' } as PermissionError);
});
}
const promises: (Promise<string> | Promise<void>)[] = [fetchData(), checkPermission()];
type AllSettledResults<T extends readonly (PromiseLike<unknown> | unknown)[]> = {
[K in keyof T]: T[K] extends PromiseLike<infer U>? {
status: 'fulfilled';
value: U;
} : {
status: 'fulfilled';
value: T[K];
} | {
status:'rejected';
reason: T[K] extends Promise<string>? NetworkError : T[K] extends Promise<void>? PermissionError : any;
};
};
function allSettled<T extends readonly (PromiseLike<unknown> | unknown)[]>(promises: T): Promise<AllSettledResults<T>> {
return Promise.allSettled(promises) as Promise<AllSettledResults<T>>;
}
allSettled(promises).then(results => {
results.forEach((result, index) => {
if (result.status ==='rejected') {
if (index === 0) {
const reason = result.reason as NetworkError;
console.log('Network error:', reason.message);
} else {
const reason = result.reason as PermissionError;
console.log('Permission error:', reason.message);
}
}
});
});
在这个示例中,我们定义了 NetworkError
和 PermissionError
两种类型来表示不同的拒绝原因。通过自定义 AllSettledResults
类型和 allSettled
函数,我们能够根据 Promise 的类型来更准确地定义 reason
的类型。这样,在处理拒绝原因时,TypeScript 可以提供更严格的类型检查,减少运行时错误的可能性。
实际应用场景中的类型演进优势
在实际项目中,比如一个电商应用,可能有多个异步操作,如获取商品列表、检查库存、验证用户优惠券等。这些操作都可能返回 Promise,并且可能因为不同原因拒绝。
// 商品类型
type Product = { id: number; name: string; price: number };
// 库存类型
type Stock = { productId: number; quantity: number };
// 优惠券类型
type Coupon = { code: string; discount: number };
// 获取商品列表
function getProducts(): Promise<Product[]> {
return new Promise((resolve, reject) => {
// 模拟网络错误
reject(new Error('Failed to fetch products'));
});
}
// 检查库存
function checkStock(productIds: number[]): Promise<Stock[]> {
return new Promise((resolve, reject) => {
// 模拟库存服务错误
reject({ type:'stock_service', message: 'Stock service unavailable' });
});
}
// 验证优惠券
function validateCoupon(couponCode: string): Promise<Coupon> {
return new Promise((resolve, reject) => {
// 模拟优惠券无效
reject(new Error('Invalid coupon code'));
});
}
// 自定义拒绝原因类型
type StockError = { type:'stock_service'; message: string };
// 自定义 `Promise.allSettled` 类型
type AllSettledResults<T extends readonly (PromiseLike<unknown> | unknown)[]> = {
[K in keyof T]: T[K] extends PromiseLike<infer U>? {
status: 'fulfilled';
value: U;
} : {
status: 'fulfilled';
value: T[K];
} | {
status:'rejected';
reason: T[K] extends Promise<Product[]>? Error : T[K] extends Promise<Stock[]>? StockError : T[K] extends Promise<Coupon>? Error : any;
};
};
function allSettled<T extends readonly (PromiseLike<unknown> | unknown)[]>(promises: T): Promise<AllSettledResults<T>> {
return Promise.allSettled(promises) as Promise<AllSettledResults<T>>;
}
const productIds = [1, 2, 3];
const couponCode = 'SAVE10';
const promises = [getProducts(), checkStock(productIds), validateCoupon(couponCode)];
allSettled(promises).then(results => {
results.forEach((result, index) => {
if (result.status ==='rejected') {
if (index === 0) {
const reason = result.reason as Error;
console.log('Error fetching products:', reason.message);
} else if (index === 1) {
const reason = result.reason as StockError;
console.log('Stock error:', reason.message);
} else {
const reason = result.reason as Error;
console.log('Coupon validation error:', reason.message);
}
}
});
});
在这个电商应用场景中,通过对 Promise.allSettled
类型的演进,我们能够更清晰地处理不同异步操作的成功和失败情况。在处理拒绝原因时,根据不同的业务逻辑,reason
有更准确的类型,这使得代码更加健壮,易于维护和调试。例如,如果库存服务的错误类型发生变化,TypeScript 会在编译时提示相关错误,而不是在运行时才发现问题。
与其他相关类型和方法的对比
- 与
Promise.all
的对比- 类型定义区别:
Promise.all
的类型定义为declare function all<T>(values: readonly (PromiseLike<T> | T)[]): Promise<T[]>;
。它返回的 Promise 成功时,值是一个与传入数组顺序相同的结果数组,只要有一个 Promise 被拒绝,整个Promise.all
就会被拒绝,并且拒绝原因是第一个被拒绝的 Promise 的原因。而Promise.allSettled
会等待所有 Promise 都敲定,返回的结果数组包含每个 Promise 的状态和结果(成功值或拒绝原因)。 - 应用场景区别:
Promise.all
适用于所有操作都必须成功才能继续的场景,例如同时获取多个用户信息,只有所有用户信息都获取成功才能进行下一步操作。而Promise.allSettled
适用于需要知道所有操作的最终状态,无论成功与否的场景,比如同时提交多个数据请求,即使部分请求失败,也想知道每个请求的结果。
- 类型定义区别:
- 与
Promise.race
的对比- 类型定义区别:
Promise.race
的类型定义为declare function race<T>(values: readonly (PromiseLike<T> | T)[]): Promise<T>;
。它返回的 Promise 会在传入的数组中第一个被敲定(完成或拒绝)的 Promise 状态确定时就敲定,其结果就是这个第一个被敲定的 Promise 的结果(成功值或拒绝原因)。 - 应用场景区别:
Promise.race
适用于只关心最先完成的操作的场景,比如同时发起多个 API 请求获取数据,只需要第一个返回的数据。而Promise.allSettled
关注所有操作的最终状态,不只是最先完成的那个。
- 类型定义区别:
未来可能的类型演进方向
- 更严格的类型推断:随着 TypeScript 类型系统的不断发展,可能会实现更严格的类型推断,使得
Promise.allSettled
在处理复杂类型的 Promise 数组时,能够更准确地推断出reason
的类型,而无需开发者手动进行过多的类型标注。 - 与新的 JavaScript 特性结合:如果 JavaScript 引入新的异步特性或改进 Promise 相关的功能,
Promise.allSettled
的类型定义可能会相应演进,以更好地支持这些新特性。例如,如果出现新的异步操作控制机制,Promise.allSettled
可能会扩展其类型定义来适配新的使用场景。 - 支持更多的并发控制场景:目前
Promise.allSettled
主要关注所有 Promise 都完成的情况。未来可能会演进其类型定义,以支持更多并发控制场景,比如限制并发数量的Promise.allSettled
变体,并且在类型上能够准确反映这些控制逻辑。
通过对 TypeScript 中 Promise.allSettled
类型演进的深入分析,我们可以看到类型定义如何随着实际需求的增长而不断改进,从而提高代码的质量和可维护性。在实际开发中,合理利用这些类型演进带来的优势,能够有效减少错误,提升开发效率。