Skip to content

Why TypeScript Result?

There are already a few quality libraries out there that provide a Result type or similar for TypeScript. We believe that there are a couple of reasons why you should consider using this library.

Ergonomic error handling

The goal is to keep the effort in using this library as light as possible, with a relatively small API surface -- you only have to learn a few methods. We don't want to introduce a whole new programming model where you would have to learn a ton of new concepts. Instead, we want to build on top of the existing features and best practices of the language, and provide a simple and intuitive API that is easy to understand and use. It also should be easy to incrementally adopt within existing codebase.

typescript
function 
processOrder
(
stock
:
Stock
,
basket
:
Basket
,
account
:
Account
) {
if (
basket
.
getTotalPrice
() >
account
.
balance
) {
return
Result
.
error
(new
InsufficientBalanceError
());
} if (!
stock
.
hasEnoughStock
(
basket
.
getProducts
())) {
return
Result
.
error
(new
NotEnoughStockError
());
} const
order
:
Order
= { /* skipped for brevity */ }
return
Result
.
ok
(
order
);
} function
handleOrder
(
userId
: number) {
const
result
=
processOrder
(
getCurrentStock
(),
getUserBasket
(
userId
),
getUserAccount
(
userId
)
); return
result
.
fold
(
(
order
) => ({
status
: 200,
body
: `Order with id ${
order
.id} placed successfully`,
}), (error) => {
Not all code paths return a value.
switch(
error
.
type
) {
case "insufficient-balance": return {
status
: 400,
body
: "Insufficient balance",
} } } ) }

In the example above, TypeScript will notify us that not all code paths return a value. Rightfully so, because we forgot to implement the case where there is not enough stock. This is a great example of how TypeScript can help us catch potential bugs early in the development process, and how using a Result type can make error handling more explicit and structured.

Let type inference do the heavy lifting

TypeScript's type inference is powerful, and we want to leverage that to make your life easier. You can just return Result.ok() or Result.error() and let TypeScript do the heavy lifting. Compared to other libraries, this means you don't have to write a lot of boilerplate code to handle the Result type, making your code cleaner and more maintainable.

Seamless async support

Result instances that are wrapped in a Promise can be painful to work with, because you would have to await every async operation before you can chain next operations (like 'map', 'fold', etc.). To solve this and to make your code more ergonomic we provide an AsyncResult that is essentially a regular Promise containing a Result type, along with a couple of methods to make it easier to chain operations without having to assign the intermediate results to a variable or having to use await for each async operation.

typescript
const firstAsyncResult = await someAsyncFunction1(); 
if (firstAsyncResult.isOk()) { 
  const secondAsyncResult = await someAsyncFunction2(firstAsyncResult.value); 
  if (secondAsyncResult.isOk()) { 
    const thirdAsyncResult = await someAsyncFunction3(secondAsyncResult.value); 
    if (thirdAsyncResult.isOk()) { 
      // do something
    } else {  
      // handle error
    }  
  } else { 
    // handle error
  } 
} else { 
  // handle error
} 
const result = await someAsyncFunction1() 
  .map((value) => someAsyncFunction2(value)) 
  .map((value) => someAsyncFunction3(value)); 

You rarely have to deal with AsyncResult directly though, because this library will automatically convert the result of an async operation to an AsyncResult when needed, and since the API's are almost identical in shape, there's a big chance you wouldn't even notice you're using a AsyncResult under the hood. Let's look at an example what this means in practice:

ts
// start with a sync result -> Result.Ok<number>
const 
result
= await
Result
.
ok
(12)
// map the value to a Promise -> AsyncResult<number, never> .
map
((
value
) =>
Promise
.
resolve
(
value
* 2)) //
// map async to another result -> AsyncResult<string, ValidationError> .
map
(async (
value
) => {
if (
value
< 10) {
return
Result
.
error
(new
ValidationError
("Value is too low"));
} return
Result
.
ok
("All good!");
});

Support for generators

Popularized by EffectTS, this library supports the use of generator functions to create and work with results. This allows you to write more imperative code that is easier to read and understand, while still benefiting from the type safety and error handling provided by the Result type. You don't have to use the generator syntax - it's fully optional. For more info, see Chaining vs. generator syntax.