Neverthrow の使い方を学ぶ

このスクラップについて
前々から気になっていた Neverthrow の使い方を学び、気づいた点などをまとめていく。

プロジェクト作成
cd ~/workspace
mkdir scrap-neverthrow
npm init -y
npm install --save-dev @types/node tsx typescript
npm install neverthrow
touch main.ts
いつも思うけど、create-typescript-app みたいなパッケージはあるのかな?
あった。

ok
import { ok } from "neverthrow";
function main() {
const myResult = ok({ myData: "test" });
console.log(myResult.isOk());
console.log(myResult.isErr());
}
main();
true
false

err
import { err } from "neverthrow";
function main() {
const myResult = err("Oh noooo");
console.log(myResult.isOk());
console.log(myResult.isErr());
}
main();
false
true

Result.map
import { err, ok } from "neverthrow";
function main() {
const linesResult = ok("1\n2\n3\n4\n".split("\n"));
const newResult = linesResult.map((lines) =>
lines.map((line) => parseInt(line, 10))
);
console.log(newResult);
console.log(newResult.isOk());
}
main();
Ok { value: [ 1, 2, 3, 4, NaN ] }
true

Result.mapErr
import { err } from "neverthrow";
function main() {
const parseResult = err("Oh noooo");
parseResult.mapErr((err) => {
console.log({ err });
});
console.log(parseResult.isErr());
}
main();
{ err: 'Oh noooo' }
true

次は Result.unwrapOr

何ともう 4 カ月も経ったのか
もうほぼ忘れてしまったので最初からやり直そう。

プロジェクト作成
cd ~/workspace
rm -rf scrap-neverthrow
mkdir scrap-neverthrow
cd scrap-neverthrow
npm init -y
pnpm i -D typescript @types/node tsx
pnpm i -D -E @biomejs/biome
pnpm biome init
touch main.ts

パッケージのインストール
pnpm i neverthrow

ok
import { ok } from "neverthrow";
async function main() {
const myResult = ok({ myData: "test" });
console.log(myResult.isOk());
console.log(myResult.isErr());
}
main().catch((err) => console.error(err));
pnpm tsx main.ts
true
false

err
import { err } from "neverthrow";
async function main() {
const myResult = err("Oh noooo");
console.log(myResult.isOk());
console.log(myResult.isErr());
}
main().catch((err) => console.error(err));
false
true

Result.map
import { type Result, ok } from "neverthrow";
async function main() {
const linesResult = getLines("1\n2\n3\n4\n");
const newResult = linesResult.map((arr) => arr.map(Number.parseInt));
console.log(newResult.isOk());
console.log(newResult.unwrapOr([]));
}
function getLines(str: string): Result<Array<string>, Error> {
return ok(str.split("\n"));
}
main().catch((err) => console.error(err));
true
[ 1, NaN, NaN, NaN, NaN ]
毎回「?」となるが、parseInt の第 2 引数に index が渡されるのでこうなるようだ。
parseInt('1', 0);
parseInt('2', 1);
parseInt('3', 2);
parseInt('4', 3);
parseInt('5', 4);
例外的に parseInt('1', 0)
だけは 0 が自動判定を意味するので成功するらしい。

次は mapErr

mapErr
import { type Result, err } from "neverthrow";
async function main() {
const rawHeaders = "nonsensical gibberish and badly formatted stuff";
const parseResult = parseHeaders(rawHeaders);
parseResult.mapErr((parseError) => {
console.log({
error: parseError,
});
});
}
function parseHeaders(raw: string): Result<Record<string, string>, Error> {
return err(new Error(raw));
}
main().catch((err) => console.error(err));
{
error: Error: nonsensical gibberish and badly formatted stuff
at parseHeaders (/Users/susukida/workspace/scrap-neverthrow/main.ts:15:14)
at main (/Users/susukida/workspace/scrap-neverthrow/main.ts:5:23)
at err (/Users/susukida/workspace/scrap-neverthrow/main.ts:18:1)
at Object.<anonymous> (/Users/susukida/workspace/scrap-neverthrow/main.ts:18:41)
at Module._compile (node:internal/modules/cjs/loader:1358:14)
at Object.transformer (/Users/susukida/workspace/scrap-neverthrow/node_modules/.pnpm/tsx@4.19.4/node_modules/tsx/dist/register-D2KMMyKp.cjs:2:1186)
at Module.load (node:internal/modules/cjs/loader:1208:32)
at Module._load (node:internal/modules/cjs/loader:1024:12)
at cjsLoader (node:internal/modules/esm/translators:348:17)
at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:297:7)
}
どうやってエラーの内容を取り出すのだろう?
結果の方は unwrapOr が利用できそうだ。

unwrapOr
import { err } from "neverthrow";
async function main() {
const myResult = err("Oh noooo");
const multiply = (value: number): number => value * 2;
const unwrapped: number = myResult.map(multiply).unwrapOr(10);
console.log(unwrapped);
}
main().catch((err) => console.error(err));
10

次は andThen から

andThen
import { type Result, err, ok } from "neverthrow";
async function main() {
const sq = (n: number): Result<number, number> => ok(n ** 2);
console.log(ok(2).andThen(sq).andThen(sq));
console.log(ok(2).andThen(sq).andThen(err));
console.log(ok(2).andThen(err));
console.log(err(3).andThen(sq).andThen(sq));
}
main().catch((err) => console.error(err));
Ok { value: 16 }
Err { error: 4 }
Err { error: 2 }
Err { error: 3 }

andThen の続き
class Result<T, E> {
andThen<U, F>(
callback: (value: T) => Result<U, F>
): Result<U, E | F> { ... }
}
- andThen 入出力の Result の型パラメーターは任意の型で良い。
- andThen メソッドの戻り値のデータ型はコールバック関数の戻り値のデータ型の方になる。
- 一方、エラー型は元のエラー型かコールバック関数の戻り値のエラー型になる。

andThen の続きの続き
import { ok } from "neverthrow";
async function main() {
const nested = ok(ok(1234));
const notNested = nested.andThen((innerResult) => innerResult);
console.log(notNested);
}
main().catch((err) => console.error(err));
Ok { value: 1234 }

asyncAndThen はスキップ
これは ResultAsync について学んでからの方が良さそう。

次は orElse から

orElse
import { type Result, err, ok } from "neverthrow";
enum DatabaseError {
PoolExhausted = "PoolExhausted",
NotFound = "NotFound",
}
async function main() {
const dbQueryResult: Result<string, DatabaseError> = err(
DatabaseError.NotFound,
);
const updatedQueryResult = dbQueryResult.orElse((dbError) =>
dbError === DatabaseError.NotFound ? ok("User does not exist") : err(500),
);
console.log(updatedQueryResult);
}
main().catch((err) => console.error(err));
Ok { value: 'User does not exist' }
andThen のエラー処理版だと思うと理解しやすいのかも知れない。
class Result<T, E> {
orElse<U, A>(
callback: (error: E) => Result<U, A>
): Result<U | T, A> { ... }
}
シグニチャについてもエラー型は E → A に変換されるが、データ型は U | T
にマージされる。

次は match から
続けてやろうと思ったがやりごたえがありそうなので次の楽しみに取っておこう。

match
import { type Result, err } from "neverthrow";
async function main() {
computationThatMightFail().map(console.log).mapErr(console.error);
computationThatMightFail().match(console.log, console.error);
}
function computationThatMightFail(): Result<string, Error> {
return err(new Error("Something went wrong"));
}
main().catch((err) => console.error(err));
Error: Something went wrong
at computationThatMightFail (/Users/susukida/workspace/scrap-neverthrow/main.ts:9:14)
at main (/Users/susukida/workspace/scrap-neverthrow/main.ts:4:3)
at err (/Users/susukida/workspace/scrap-neverthrow/main.ts:12:1)
at Object.<anonymous> (/Users/susukida/workspace/scrap-neverthrow/main.ts:12:41)
at Module._compile (node:internal/modules/cjs/loader:1358:14)
at Object.transformer (/Users/susukida/workspace/scrap-neverthrow/node_modules/.pnpm/tsx@4.19.4/node_modules/tsx/dist/register-D2KMMyKp.cjs:2:1186)
at Module.load (node:internal/modules/cjs/loader:1208:32)
at Module._load (node:internal/modules/cjs/loader:1024:12)
at cjsLoader (node:internal/modules/esm/translators:348:17)
at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:297:7)
Error: Something went wrong
at computationThatMightFail (/Users/susukida/workspace/scrap-neverthrow/main.ts:9:14)
at main (/Users/susukida/workspace/scrap-neverthrow/main.ts:5:3)
at err (/Users/susukida/workspace/scrap-neverthrow/main.ts:12:1)
at Object.<anonymous> (/Users/susukida/workspace/scrap-neverthrow/main.ts:12:41)
at Module._compile (node:internal/modules/cjs/loader:1358:14)
at Object.transformer (/Users/susukida/workspace/scrap-neverthrow/node_modules/.pnpm/tsx@4.19.4/node_modules/tsx/dist/register-D2KMMyKp.cjs:2:1186)
at Module.load (node:internal/modules/cjs/loader:1208:32)
at Module._load (node:internal/modules/cjs/loader:1024:12)
at cjsLoader (node:internal/modules/esm/translators:348:17)
at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:297:7)
match も map + mapErr もほぼ一緒だが、match の方はエラー処理も必ず書かなければいけない点が異なる。

match の続き
import { type Result, err } from "neverthrow";
async function main() {
const attempt = computationThatMightFail()
.map((str) => str.toUpperCase())
.mapErr((err) => `Error: ${err}`);
const answer = computationThatMightFail().match(
(str) => str.toUpperCase(),
(err) => `Error: ${err}`,
);
console.log(attempt);
console.log(answer);
}
function computationThatMightFail(): Result<string, Error> {
return err(new Error("Something went wrong"));
}
main().catch((err) => console.error(err));
Err { error: 'Error: Error: Something went wrong' }
Error: Error: Something went wrong
map + mapErr が Result 型になるのに対し、match は string 型になる。
もし match のエラー処理で別の型を返した場合、例えば Error を返した場合は string | Error になる。

match のさらに続き
match でエラー値を使わないのであれば map + unwrapOr と同じだが、match の方が忘れることがないので良さそう。
import { type Result, err } from "neverthrow";
async function main() {
const answer1 = computationThatMightFail().match(
(str) => str.toUpperCase(),
() => "ComputationError",
);
const answer2 = computationThatMightFail()
.map((str) => str.toUpperCase())
.unwrapOr("ComputationError");
console.log(answer1);
console.log(answer2);
}
function computationThatMightFail(): Result<string, Error> {
return err(new Error("Something went wrong"));
}
main().catch((err) => console.error(err));
ComputationError
ComputationError

次は andTee
asyncMap は一旦スキップしよう。

andTee
import { type Result, err, ok } from "neverthrow";
type RequestData = Record<string, never>;
type User = {
name: string;
};
type ParseError = "ParseError";
type LogError = "LogError";
type InsertError = "InsertError";
async function main() {
const userInput: RequestData = {};
const result = parseUserInput(userInput).andTee(logUser).andThen(insertUser);
console.log(result);
}
function parseUserInput(_input: RequestData): Result<User, ParseError> {
return ok({ name: "John Doe" });
}
function logUser(user: User): Result<void, LogError> {
console.log(`insertUser: ${user.name}`);
return ok();
}
function insertUser(user: User): Result<void, InsertError> {
return ok();
}
main().catch((err) => console.error(err));
insertUser: John Doe
Ok { value: undefined }
andTee はログなどの副作用を扱うのに便利。
result
のデータ型は Result<void, "ParseError" | "InsertError">
となり、LogError
が含まれない。

orTee
import { type Result, err, ok } from "neverthrow";
type RequestData = Record<string, never>;
type User = {
name: string;
};
type ParseError = "ParseError";
type LogError = "LogError";
type InsertError = "InsertError";
async function main() {
const userInput: RequestData = {};
const result = parseUserInput(userInput)
.orTee(logParseError)
.andThen(insertUser)
.orTee(logInsertError);
console.log(result);
}
function parseUserInput(_input: RequestData): Result<User, ParseError> {
return err("ParseError");
}
function logParseError(_err: ParseError): Result<void, LogError> {
console.log("parseUserInput failed");
return ok();
}
function insertUser(_user: User): Result<void, InsertError> {
console.log("insertUser called");
return ok();
}
function logInsertError(
_err: ParseError | InsertError,
): Result<void, LogError> {
console.log("insertUser failed");
return ok();
}
main().catch((err) => console.error(err));
parseUserInput failed
insertUser failed
Err { error: 'ParseError' }
orTee は andTee のエラー処理バージョンと考えて良さそうだ。
ちょっとややこしいなと思ったのは logInsertError
関数の引数データ型が ParseError | InsertError
である点。
ChatGPT に聞いたら下記のようなコードを提案された。
async function main() {
const userInput: RequestData = {};
const parsed = parseUserInput(userInput);
if (parsed.isErr()) {
logParseError(parsed.error);
console.log(parsed);
return;
}
const inserted = insertUser(parsed.value);
if (inserted.isErr()) {
logInsertError(inserted.error);
}
console.log(inserted);
}
なるほど普通に書けば良いのだが、なんかこれだとあまり意味がない感じがする。
結局結論としてはログ出力したいのであれば最後に orTee だけを書けば良いということになりそうだ。

andThrough
import { type Result, err, ok } from "neverthrow";
type RequestData = Record<string, never>;
type User = {
name: string;
};
type ParseError = "ParseError";
type ValidationError = "ValidationError";
type InsertError = "InsertError";
async function main() {
const userInput: RequestData = {};
const result = parseUserInput(userInput)
.andThrough(validateUser)
.andThen(insertUser);
console.log(result);
}
function parseUserInput(_input: RequestData): Result<User, ParseError> {
return ok({ name: "John Doe" });
}
function validateUser(_user: User): Result<void, ValidationError> {
return err("ValidationError");
}
function insertUser(_user: User): Result<void, InsertError> {
console.log("insertUser called");
return ok();
}
main().catch((err) => console.error(err));
Err { error: 'ValidationError' }
andThrough は andTee に似ていて結果がそのまま後の処理に渡されるが、エラーが発生した場合は andThen のようになる点が異なる。
バリデーションのようなケースで使えそうだ。

次は fromThrowable
この関数は絶対欲しいと思っていたものなのでじっくり学んでいこう。

fromThrowable
Although Result is not an actual JS class, the way that fromThrowable has been implemented requires that you call fromThrowable as though it were a static method on Result. See examples below.
理解が間違っているかも知れないが、fromThrowable
関数は Result.fromThrowable()
のように呼び出さないと行けないのかも知れない。
import { Result } from "neverthrow";
async function main() {
const safeParseJson = Result.fromThrowable(JSON.parse, (err) =>
err instanceof Error ? err : new Error(String(err)),
);
const res = safeParseJson("{");
console.log(res);
}
main().catch((err) => console.error(err));
Err {
error: SyntaxError: Expected property name or '}' in JSON at position 1
at parse (<anonymous>)
at /Users/susukida/workspace/scrap-neverthrow/node_modules/.pnpm/neverthrow@8.2.0/node_modules/neverthrow/dist/index.cjs.js:309:32
at main (/Users/susukida/workspace/scrap-neverthrow/main.ts:8:15)
at <anonymous> (/Users/susukida/workspace/scrap-neverthrow/main.ts:13:1)
at Object.<anonymous> (/Users/susukida/workspace/scrap-neverthrow/main.ts:13:41)
at Module._compile (node:internal/modules/cjs/loader:1241:14)
at Object.transformer (/Users/susukida/workspace/scrap-neverthrow/node_modules/.pnpm/tsx@4.19.4/node_modules/tsx/dist/register-D2KMMyKp.cjs:2:1186)
at Module.load (node:internal/modules/cjs/loader:1091:32)
at Module._load (node:internal/modules/cjs/loader:938:12)
at cjsLoader (node:internal/modules/esm/translators:284:17)
}

次は combine