MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript Promise.allSettled类型演进

2022-12-215.3k 阅读

理解 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 类型定义中 reasonany,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;
    };
}>;

这里使用了泛型和条件类型来使类型定义更加灵活和准确。主要有以下几点改进:

  1. 泛型数组类型promises 参数的类型从 Array<PromiseLike<T>> 变为 readonly (PromiseLike<T> | T)[],这意味着不仅可以传入 PromiseLike<T> 类型的数组,还可以传入包含直接值(非 Promise)的数组,并且该数组是只读的,防止意外修改。
  2. 条件类型处理:第二个重载使用了条件类型和映射类型。对于 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);
            }
        }
    });
});

在这个示例中,我们定义了 NetworkErrorPermissionError 两种类型来表示不同的拒绝原因。通过自定义 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 会在编译时提示相关错误,而不是在运行时才发现问题。

与其他相关类型和方法的对比

  1. 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 适用于需要知道所有操作的最终状态,无论成功与否的场景,比如同时提交多个数据请求,即使部分请求失败,也想知道每个请求的结果。
  2. Promise.race 的对比
    • 类型定义区别Promise.race 的类型定义为 declare function race<T>(values: readonly (PromiseLike<T> | T)[]): Promise<T>;。它返回的 Promise 会在传入的数组中第一个被敲定(完成或拒绝)的 Promise 状态确定时就敲定,其结果就是这个第一个被敲定的 Promise 的结果(成功值或拒绝原因)。
    • 应用场景区别Promise.race 适用于只关心最先完成的操作的场景,比如同时发起多个 API 请求获取数据,只需要第一个返回的数据。而 Promise.allSettled 关注所有操作的最终状态,不只是最先完成的那个。

未来可能的类型演进方向

  1. 更严格的类型推断:随着 TypeScript 类型系统的不断发展,可能会实现更严格的类型推断,使得 Promise.allSettled 在处理复杂类型的 Promise 数组时,能够更准确地推断出 reason 的类型,而无需开发者手动进行过多的类型标注。
  2. 与新的 JavaScript 特性结合:如果 JavaScript 引入新的异步特性或改进 Promise 相关的功能,Promise.allSettled 的类型定义可能会相应演进,以更好地支持这些新特性。例如,如果出现新的异步操作控制机制,Promise.allSettled 可能会扩展其类型定义来适配新的使用场景。
  3. 支持更多的并发控制场景:目前 Promise.allSettled 主要关注所有 Promise 都完成的情况。未来可能会演进其类型定义,以支持更多并发控制场景,比如限制并发数量的 Promise.allSettled 变体,并且在类型上能够准确反映这些控制逻辑。

通过对 TypeScript 中 Promise.allSettled 类型演进的深入分析,我们可以看到类型定义如何随着实际需求的增长而不断改进,从而提高代码的质量和可维护性。在实际开发中,合理利用这些类型演进带来的优势,能够有效减少错误,提升开发效率。