Open34

Neverthrow の使い方を学ぶ

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

プロジェクト作成

コマンド
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
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Result.map

https://github.com/supermacro/neverthrow?tab=readme-ov-file#resultmap-method

main.ts
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 が自動判定を意味するので成功するらしい。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

mapErr

main.ts
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 が利用できそうだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

andThen

main.ts
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 }
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

andThen の続き

シグニチャ
class Result<T, E> {
  andThen<U, F>(
    callback: (value: T) => Result<U, F>
  ): Result<U, E | F> { ... }
}
  • andThen 入出力の Result の型パラメーターは任意の型で良い。
  • andThen メソッドの戻り値のデータ型はコールバック関数の戻り値のデータ型の方になる。
  • 一方、エラー型は元のエラー型かコールバック関数の戻り値のエラー型になる。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

andThen の続きの続き

main.ts
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 }
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

orElse

main.ts
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 にマージされる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

match

main.ts
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 の方はエラー処理も必ず書かなければいけない点が異なる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

match の続き

main.ts
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 になる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

match のさらに続き

match でエラー値を使わないのであれば map + unwrapOr と同じだが、match の方が忘れることがないので良さそう。

main.ts
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
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

andTee

main.ts
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 が含まれない。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

orTee

https://github.com/supermacro/neverthrow?tab=readme-ov-file#resultortee-method

main.ts
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 だけを書けば良いということになりそうだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

andThrough

main.ts
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 のようになる点が異なる。

バリデーションのようなケースで使えそうだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

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() のように呼び出さないと行けないのかも知れない。

main.ts
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)
}