Last time, we looked at general options for error handling in TypeScript. I liked Rust style Result<T,E>
types.
I was left with a choice of rolling my own minimal implementation or using neverthrow or true-myth. Both neverthrow
and true-myth
are small, self-contained packages that follow the Rust conventions closely.
type Result<T,E> = Ok<T,E> | Err<T,E>
They both provide Ok
and Err
classes with a set of utility methods for working with results. They both provide ok()
and err()
convenience methods for creating instances of Ok
and Err
.
If I end up writing my own, I’ll do the same.
Spreadsheet Data
My SpreadsheetData
interface currently has one method, setCellValueAndFormat
, that allows you to edit data. Currently, it just returns a boolean
for success or failure.
export interface SpreadsheetData<Snapshot> {
setCellValueAndFormat(row: number, column: number,
value: CellValue, format: string | undefined): boolean;
}
I want to add more formal error handling so that I can report meaningful error messages in the UI. I’m going to use this method as a test case. However, there’s an immediate problem. There’s no value to return for the success case. How do you handle that?
I could return Error | null
but that feels backwards. The return value is “falsy” on success, which is immediately counter-intuitive. This is also the kind of ad hoc approach I’m trying to get away from.
It will be interesting to see how the two libraries handle this case. For now, I’ll just return a string
for the error case. The point is to focus on the differences between the libraries. Once I have a winner, or decide to roll my own, I can figure out what a full blown SpreadsheetDataError
type should look like.
Neverthrow
I’m starting with neverthrow
. It’s the smallest library of the two and the most popular. It has 850K npm weekly downloads, 408 dependent packages using it, 5.2K GitHub stars and 49 contributors. The package is currently at version 8.2.0 and has had 63 versions released over 6 years.
There is minimal documentation, most of which is embedded in the README. There’s no Intellisense for the key entry points: Result
, Ok
, Err
, ok
, err
. However, scanning through the README gave me enough to get started.
The example code for some of the utility methods showed me how to handle functions that normally return void. It turned out to be surprisingly simple. If you have a function that returns a number
on success and a string
on error, you’d use a Result<number,string>
. Similarly, if the function returns void
on success, you’d use a Result<void,string>
.
import { Result, err } from "neverthrow";
export class EmptySpreadsheetData implements SpreadsheetData<number> {
setCellValueAndFormat(row, column, value, format): Result<void,string>
{
if (isValid(value)) {
...
return ok();
} else {
return err("Invalid value"));
}
}
}
Note that the ok()
constructor needs no arguments if the success return is void
. In most cases, TypeScript can infer the type parameters for ok
and err
.
Replacing the boolean
return with Result<void,string>
increased the bundle size for my spreadsheet sample app by 7KB. There are lots of convenience methods that support inter-operation between Result
and ResultAsync
. The end result is that the whole of neverthrow
gets pulled in if you use any part of it. It’s a good job it’s not that big.
The next question is how to make neverthrow
feel like a natural part of InfiniSheet rather than something external grafted on. The first step is to re-export the main entry points from infinisheet-types
. Then clients can import Result
at the same time they import SpreadsheetData
.
I realized that I could provide my own documentation/intellisense for the main entry points by defining type aliases and wrapper functions. I created my own Result.ts
source file in infinisheet-types
and restricted direct access to neverthrow
to that file. It also means I can change implementation in future with minimal impact.
import type { Ok as neverthrow_Ok, Err as neverthrow_Err } from "neverthrow";
import { err as neverthrow_err, ok as neverthrow_ok } from "neverthrow";
/** An `Ok` instance is the *successful* variant of the {@link Result} type */
export interface Ok<T,E> extends neverthrow_Ok<T,E> {}
/** An `Err` instance is the failure variant of the {@link Result} type */
export interface Err<T,E> extends neverthrow_Err<T,E> {}
/** A `Result` represents success ({@link Ok}) or failure ({@link Err}) */
export type Result<T,E> = Ok<T,E> | Err<T,E>;
/** Create an instance of {@link Ok} */
export function ok<T, E = never>(value: T): Ok<T, E>
export function ok<_T extends void = void, E = never>(value: void): Ok<void, E>
export function ok<T, E = never>(value: T): Ok<T, E> {
return neverthrow_ok<T,E>(value);
}
/** Create an instance of {@link Err} */
export function err<T = never, E extends string = string>(err: E): Err<T, E>
export function err<T = never, E = unknown>(err: E): Err<T, E>
export function err<T = never, _E extends void = void>(err: void): Err<T, void>
export function err<T = never, E = unknown>(err: E): Err<T, E> {
return neverthrow_err<T,E>(err)
}
The magic of TypeScript structural typing means that the neverthrow
original types and my wrapper types interoperate nicely.
It turns out that neverthrow
does have decent embedded JSDoc comments for the Ok
and Err
utility methods. I tweaked my TypeDoc externalPattern
configuration so that we can pull in the neverthrow
documentation for methods from the inherited Ok
and Err
classes, while continuing to exclude React.


True Myth
true-myth
is a couple of years older than neverthrow
. It’s currently on version 8.5.3 with 77 released versions over 8 years. However, it hasn’t gained as much traction as neverthrow
. It has 300K npm weekly downloads, only 56 dependent packages using it, 1.1K GitHub stars and 22 contributors.
In contrast to neverthrow
, the documentation is excellent. The docs are generated from JSDoc comments using TypeDoc, with full Intellisense in VS Code.
As with neverthrow
, I declared the return type as Result<void,string>
. That seemed fine, with no resulting type check errors. Returning an error works as expected, return err("Invalid value")
. However, return ok()
results in the incomprehensible error "type Result<Unit,never> is not assignable to type Result<void,string>"
. After some experimentation I got it to work by using return ok(undefined)
but it’s hardly intuitive.
Unit is a common concept in functional languages. All functions return a value, so how do you type a function that has nothing to return? You return Unit
. Unit
is a type with a single valid value, also called Unit
. Similarly, boolean
is a type with two valid values, true
and false
.
Returning a type with a single valid value tells you nothing interesting. Which is why classic functional languages use returning Unit
as a convention for functions that have nothing meaningful to return. It avoids the need to add a special case like void
to the language.
True Myth would really like you to use Result<Unit,string>
as your result type. The ok()
convenience method is hardwired to return a Result<Unit,never>
when called with no arguments.
Unit
is a separate module, so if you use this style you need two separate import statements as well as the two separate types.
import { Result, ok, err} from 'true-myth/result`
import { Unit } from `true-myth/unit`
The separation into modules does mean that you only pay for what you use. Adding true-myth
only increased the bundle size for my Spreadsheet sample app by 2KB. Using Result
doesn’t pull in True Myth’s equivalent of ResultAsync
. The downside is slightly more verbose code when you need to use multiple true-myth
modules.
As with neverthrow
, I can use a wrapper to re-export the required types from infinisheet-types
. However, you’re still adding conceptual complexity. I’d need to explain what this wacky Unit
thing is to someone who just wants to use a React spreadsheet component.
I could wrap ok()
so that it returns a Result<void,string>. However, it feels like I’m going against the grain. The documentation is full of references to Unit
and other functional concepts. True Myth really wants to convert you to the joys of functional programming by reinventing core TypeScript features.
Rolling my Own
If I rolled my own implementation, it would end up being a subset of neverthrow
. At this point, I can’t see any point in doing that. If I need to do it in future, I can copy and paste whatever I want to take from neverthrow
into Result.ts
.
Best of Both
So, I’ve decided to go with neverthrow
. The only real downside is the minimal documentation. Wrapping and re-exporting the main entry points gives me a chance of addressing that.
I was wondering where to start with writing my own documentation when I realized that the true-myth
documentation applied equally well to neverthrow
. That is, after a few minor tweaks to remove all mentions of Unit
.


Next Time
All that remains is to use this in anger. I need to add a proper SpreadsheetDataError
type, add some failure cases to my sample SpreadsheetData
implementations and then handle failure gracefully in my React spreadsheet component.