A TypeScript utility that wraps Promise.allSettled and provides strongly-typed discriminated unions for success and failure results, with type guard helpers for easy filtering.
// Strongly-typed result types using discriminated unions
interface SettledSuccess<T> {
status: 'fulfilled';
value: T;
index: number;
}
interface SettledFailure {
status: 'rejected';
reason: Error;
index: number;
}
type SettledResult<T> = SettledSuccess<T> | SettledFailure;
interface AllSettledResult<T> {
results: SettledResult<T>[];
successes: SettledSuccess<T>[];
failures: SettledFailure[];
values: T[];
errors: Error[];
}
// Type guards for filtering results
function isSuccess<T>(result: SettledResult<T>): result is SettledSuccess<T> {
return result.status === 'fulfilled';
}
function isFailure<T>(result: SettledResult<T>): result is SettledFailure {
return result.status === 'rejected';
}
// Main utility function
async function allSettledTyped<T>(
promises: Promise<T>[]
): Promise<AllSettledResult<T>> {
const nativeResults = await Promise.allSettled(promises);
const results: SettledResult<T>[] = nativeResults.map((result, index) => {
if (result.status === 'fulfilled') {
return {
status: 'fulfilled' as const,
value: result.value,
index,
};
}
return {
status: 'rejected' as const,
reason: result.reason instanceof Error
? result.reason
: new Error(String(result.reason)),
index,
};
});
const successes = results.filter(isSuccess);
const failures = results.filter(isFailure);
return {
results,
successes,
failures,
values: successes.map((s) => s.value),
errors: failures.map((f) => f.reason),
};
}
// Overload for handling tuple of different promise types
async function allSettledTuple<T extends readonly unknown[]>(
promises: { [K in keyof T]: Promise<T[K]> }
): Promise<{ [K in keyof T]: SettledResult<T[K]> }> {
const nativeResults = await Promise.allSettled(promises);
return nativeResults.map((result, index) => {
if (result.status === 'fulfilled') {
return {
status: 'fulfilled' as const,
value: result.value,
index,
};
}
return {
status: 'rejected' as const,
reason: result.reason instanceof Error
? result.reason
: new Error(String(result.reason)),
index,
};
}) as { [K in keyof T]: SettledResult<T[K]> };
}
// Usage example with mixed resolve/reject
async function demonstrateUsage(): Promise<void> {
// Simulated async operations
const fetchUser = (id: number): Promise<{ id: number; name: string }> =>
id > 0
? Promise.resolve({ id, name: `User ${id}` })
: Promise.reject(new Error(`Invalid user ID: ${id}`));
const fetchScore = (userId: number): Promise<number> =>
userId % 2 === 0
? Promise.resolve(userId * 100)
: Promise.reject(new Error(`Score unavailable for user ${userId}`));
// Example 1: Homogeneous array of promises
console.log('--- Example 1: User fetches ---');
const userPromises = [fetchUser(1), fetchUser(-1), fetchUser(2), fetchUser(0)];
const userResults = await allSettledTyped(userPromises);
console.log(`Total: ${userResults.results.length}`);
console.log(`Successes: ${userResults.successes.length}`);
console.log(`Failures: ${userResults.failures.length}`);
userResults.successes.forEach((s) => {
console.log(`[${s.index}] Loaded: ${s.value.name}`);
});
userResults.failures.forEach((f) => {
console.log(`[${f.index}] Error: ${f.reason.message}`);
});
// Example 2: Using tuple version for heterogeneous promises
console.log('\n--- Example 2: Mixed types tuple ---');
const [userResult, scoreResult] = await allSettledTuple([
fetchUser(2),
fetchScore(3),
] as const);
if (isSuccess(userResult)) {
console.log(`User: ${userResult.value.name}`);
}
if (isFailure(scoreResult)) {
console.log(`Score error: ${scoreResult.reason.message}`);
}
// Example 3: Partial success handling pattern
console.log('\n--- Example 3: Partial success pattern ---');
const scorePromises = [fetchScore(2), fetchScore(3), fetchScore(4), fetchScore(5)];
const scoreResults = await allSettledTyped(scorePromises);
if (scoreResults.failures.length > 0) {
console.warn(
`Warning: ${scoreResults.failures.length} scores failed to load`
);
}
const totalScore = scoreResults.values.reduce((sum, score) => sum + score, 0);
console.log(`Total score from successful fetches: ${totalScore}`);
}
// Run the demonstration
demonstrateUsage().catch(console.error);This utility addresses a common pain point with the native Promise.allSettled API: while it correctly handles mixed success/failure scenarios, its return type requires manual narrowing and doesn't provide convenient access to filtered results. Our wrapper maintains full type safety while offering a more ergonomic interface.
The core design uses discriminated unions (SettledSuccess and SettledFailure) with the 'status' field as the discriminant. This pattern enables TypeScript's control flow analysis to automatically narrow types when you check the status property. We also add an 'index' field to each result, which is invaluable for correlating results back to their original promises—something the native API doesn't provide directly.
The AllSettledResult interface returns multiple views of the same data: the raw results array for iteration, pre-filtered successes and failures arrays for when you need to process them separately, and extracted values/errors arrays for the common case where you just need the unwrapped data. This follows the principle of making the common case easy while keeping the full data available.
One subtle but important detail is the error normalization in the failure handling. Promise rejections can technically be any value (not just Error objects), so we wrap non-Error rejections in an Error instance. This ensures consistent typing and prevents runtime surprises when accessing error properties. The 'as const' assertions ensure TypeScript infers literal types for the status strings rather than widening to 'string'.
The tuple overload (allSettledTuple) handles the case where you're awaiting promises of different types and want to preserve type information for each position. This is useful for parallel fetches of related but differently-typed data. Use the array version (allSettledTyped) when processing homogeneous collections, and the tuple version when you have a fixed set of heterogeneous promises where positional typing matters.