Handling asynchronous operations in TypeScript can benefit from a more type-safe and readable approach. In this article, we will explore a piece of code that demonstrates how to combine multiple asynchronous operations and handle their results in a type-safe manner.
import { Result, Err, Ok } from 'neverthrow'
type CombinePromisesResult<D extends Record<string, unknown>, E> = {
[P in keyof D]: Result<UnwrapPromise<D[P]>, E>
}
type UnwrapPromise<P extends unknown> = P extends PromiseLike<infer V> ? V : P
export const combinePromises = async <
D extends Record<string, unknown>,
E = Error,
>(obj: D): Promise<CombinePromisesResult<D, E>> => {
const keys = Object.keys(obj)
const promises = keys.map(
(key) =>
obj[key as keyof D] as Promise<Result<UnwrapPromise<D[keyof D]>, E>>
)
const results = await Promise.all(promises)
const data: Record<string, Result<UnwrapPromise<D[keyof D]>, E>> = {}
results.forEach((result, i) => {
data[keys[i]] = result
})
return data as CombinePromisesResult<D, E>
}
export type User = {
id: string
name: string
}
export const fetchUser = (): Promise<Result<User, Error>> => {
const data: User = {
id: "1",
name: "jet"
}
// return Promise.resolve(new Ok(data))
return Promise.resolve(new Err(new Error('Failed to fetch user.')))
}
export type Comment = {
id: string
content: string
reviewedBy: string
}
export const fetchComments = (): Promise<Result<Comment[], Error>> => {
const data: Comment[] = [{
id: "1",
content: "a",
reviewedBy: "fei"
}, {
id: "2",
content: "b",
reviewedBy: "spike"
}]
return Promise.resolve(new Ok(data))
return Promise.resolve(new Err(new Error('Failed to fetch comments.')));
}
export type Company = {
id: string
name: string
}
export const fetchCompany = (): Promise<Result<Company, Error>> => {
const data: Company = {
id: "1",
name: "cowboy bebop"
}
return Promise.resolve(new Ok(data))
return Promise.resolve(new Err(new Error('Failed to fetch company.')))
}
Introduction to the neverthrow
Library
At the beginning of the code, we import three types/classes from neverthrow
: Result
, Err
, and Ok
. The neverthrow
library assists in functional error handling, using the Result
type to represent two states: success and failure.
Type Definitions
CombinePromisesResult
This type is responsible for associating the result of asynchronous operations with each property of an object. The result is of type Result
, holding either successful data or an error.
UnwrapPromise
A utility type designed to extract the type inside a Promise. For instance, given Promise<string>
, this type will produce string
.
The combinePromises
Function
This function aims to execute multiple asynchronous operations in parallel and return their results in an aggregated object.
Each property of the object is expected to represent a Promise of an asynchronous operation.
We employ
Promise.all
to execute all asynchronous operations in parallel.Eventually, the resultant object is returned.
Sample Functions
fetchUser
,fetchComments
, andfetchCompany
are functions that simulate asynchronous operations.Currently,
fetchUser
is set to return an error, but you can uncomment a line to test the successful scenario.Similarly,
fetchComments
andfetchCompany
have both successful and unsuccessful cases, but the unsuccessful scenarios are commented out for now.
Conclusion
The provided code illustrates a pattern of handling multiple asynchronous operations in a type-safe manner. By leveraging TypeScript's advanced type features, you can write error-handling logic that's both safe and concise.