neverthrow と fetch と zod を組み合わせた非同期処理
こんにちは。フロントエンドエンジニアの辻です。
Gaji-Labo ではデザイナーとエンジニアが協力して、「手ざわりのいいUI」の実現に向けて、日々励んでいます。
フロントエンドエンジニアとしては、デザイン通りに実装するのはもちろんの事、UI の裏側で動作するロジックにも同じように注力しています。今回はその裏側のロジックについての話です。
はじめに、フロントエンドにおけるロジック実装の一手として neverthrow というライブラリを紹介します。
次に neverthrow と fetch と zod を組み合わせた非同期処理を書いていきます。
neverthrow とは?
neverthrow とは、JavaScript(TypeScript)においてエラーをスローせずに Result 型として扱うライブラリです。その名の通りですね。
neverthrow を導入すると、JavaScript(TypeScript)の try ~ catch 構文を、Rust 言語における Result 型に似た機能で代用できるため、より安全で堅牢なコードを記述できます。
> neverthrow 公式ページ
> Result – Rust By Example
以下は neverthrow のサンプルコードです。
import { Result, ok, err } from "neverthrow";
/**
* 引数の target を numbers 配列のいずれかの値で除算する関数です。
* もし 0 で除算しようとした場合は err() を返します。
* 0 以外で除算する場合は ok() を返します。
*/
const divide = (target: number): Result<number, string> => {
const numbers = [0, 1, 2];
const index = Math.floor(Math.random() * numbers.length);
if (numbers[index] === 0) return err("0で割ることはできません。");
return ok(target / numbers[index]);
};
/**
* isOk() は処理が成功したか(okを返したか)を判定します。
*/
const result = divide(10);
if (result.isOk()) {
console.log(result.value); // 10 or 5
} else {
console.log(result.error); // 0で割ることはできません。
}
neverthrow と非同期処理を組み合わせてみる
とても便利な neverthrow ですが、私は非同期処理と組み合わせて使っています。
非同期処理はそれ自体の難易度もさることながら、エラーハンドリングも考慮すると一筋縄ではいきません。
そこで neverthrow の出番です。
さっそくサンプルコードを用意しました。
import { Result, ok, err } from "neverthrow";
/**
* neverthrow と fetch を組み合わせた関数です。
* ジェネリクスの R は API のレスポンスの型。P はリクエストパラメータの型です。
*/
export const handleFetch = <R, P = unknown>({
path,
method = "GET",
requestParams,
}: {
path: string;
method?: "GET" | "POST" | "PUT" | "DELETE";
requestParams?: P;
}): Promise<Result<R, Error>> => {
// リクエストパラメータの整備
const params: RequestInit = {
method,
};
if ((method === "POST" || method === "PUT") && requestParams) {
params.body = JSON.stringify(requestParams);
}
// fetch の実行
return fetch(path, params)
.then((res) => {
if (!res.ok) {
// 通信に失敗した場合は例外を投げます
// 最終的に、例外は catch 句にて err() でラップして返します
throw new Error("サーバーとの通信に失敗しました");
}
return res.json();
})
.then((data: R) => {
return ok(data);
})
.catch((error: Error) => {
return err(error);
});
};
今回用意した handleFetch 関数ですが、リクエストパラメータの整備や fetch の実行については、一般的な非同期処理と変わりありません。
neverthrow を導入した事による変化として、戻り値が ok()
か err()
でラップされている点が挙げられます。
これにより、handleFetch 関数の戻り値の型は Promise<Result<R, Error>>
となり、handleFetch 関数の呼び出し元では isOk()
や isErr()
が扱えます。
実際に使ってみるとこんな感じです。
import { handleFetch } from "./neverthrow_fetch";
type Todo = {
id: number;
userId: number;
title: string;
completed: boolean;
};
const run = async () => {
const result = await handleFetch<Todo>({
path: "https://jsonplaceholder.typicode.com/todos/1",
});
if (result.isOk()) {
console.log(result.value); // { "userId": 1, "id": 1, "title": "title text", "completed": false }
} else {
console.log(result.error);
}
};
run();
ちなみに、neverthrow は非同期の Result 型を表現する機能として ResultAsync
を用意しています。
今回は分かりやすさを優先して Promise<Result<T, E>>
としていますが、開発時は ResultAsync<T, E>
とした方がより効率的でしょう。
> Asynchronous API (ResultAsync) | neverthrow
zod と組み合わせてみる
最後に handleFetch 関数と zod を組み合わせて使ってみます。
zod はスキーマのバリデーションライブラリです。シンプルな使い勝手ながらも機能は強力です。
先程の handleFetch 関数と zod を組み合わせたサンプルコードです。
import { err, ok, fromPromise } from "neverthrow";
import { z, ZodError } from "zod";
import { handleFetch } from "./neverthrow_fetch";
type Todo = {
id: number;
userId: number;
title: string;
completed: boolean;
};
const run = (id: number) => {
ok(z.number().min(1).max(100).safeParse(id))
.andThen((id) => {
// URL 末尾に入る id を zod で検証して、その結果を ok() か err() に振り分ける … (1)
if (!id.success) return err(id.error);
return ok(id.data);
})
.asyncAndThen((id) => {
// 検証後の id を利用して API を実行する … (2)
// fromPromise は Promise<Result<Todo, Error>> を ResultAsync として扱うため
return fromPromise(
handleFetch<Todo>({
path: `https://jsonplaceholder.typicode.com/todos/${id}`,
}),
(error) => error
);
})
.map((result) => {
// 一連の処理が成功した場合 … (3)
if (result.isOk()) {
console.log(result.value);
}
})
.mapErr((error) => {
// エラーが発生した場合 … (4)
if (error instanceof ZodError) {
console.log("zod エラー", error);
} else {
console.log("API エラー", error);
}
});
};
run(1);
zod の safeParse 関数の結果を、ok()
に入れる所からはじめて、
- URL 末尾に入る id を zod で検証して、その結果を
ok()
かerr()
に振り分ける - zod の検証が成功後に、その結果を利用して API を実行する
- 一連の処理が成功した場合、その結果を出力する
- エラーが発生した場合、
instanceof
構文でエラーの種別を判定して、エラーハンドリングを行う
…の流れになっています。
今回利用した neverthrow の andThen、asyncAndThen、map、mapErr の機能は、それぞれ次の通りです。
andThen と asyncAndThen
andThen 関数は、Result 型オブジェクトに対して連続した操作をするために利用します。イメージとしては非同期処理における then が近いですね。
andThen 関数は、Result 型オブジェクトが Ok の場合にのみ、引数の関数を実行します。もし Result 型オブジェクトが Err であれば引数の関数は実行されず、そのまま Err を返します。
先程のサンプルコードでは、zod で検証した結果を ok()
か err()
に振り分けています。
また、andThen 関数が Result 型オブジェクトを扱うのに対して、asyncAndThen 関数は ResultAsync 型オブジェクトを扱います。
handleFetch 関数の実行時に asyncAndThen 関数を利用しているのは、そのためです。
> Result.asyncAndThen | neverthrow
map
map 関数も andThen 関数と似たような動きをします。
map 関数も Result 型オブジェクトが Ok の場合に、引数の関数を実行します。もし Result 型オブジェクトが Err であれば引数の関数は実行されず、そのまま Err を返します。
andThen 関数がより複雑な処理に向いているのに対して、map 関数は Ok の値を変換する処理に向いています。(今回はただログを出力しているだけですが…)
andThen も asyncAndThen も map も、Result 型オブジェクトが Ok の場合のみ、引数の関数を発火させます。途中で処理が失敗して Err になっていた場合は、引数に指定した関数は発火しません。
先程のサンプルコードでは、run 関数の引数に 1 〜 100 以外の数値を入れると Zod エラーが発生するため、後続の handleFetch 関数が発火しないようになっています。
mapErr
map 関数と対となるのが mapErr 関数です。
mapErr 関数は Result 型オブジェクトが Err の場合に引数の関数を実行します。
先程のサンプルコードでは、Zod エラーと API エラーの 2 つをまとめて mapErr 関数で処理しています。
まとめ
簡単なサンプルではありますが、neverthrow と fetch の組み合わせから、zod との併用までを見てきました。
非同期処理に neverthrow を導入すれば、パラメータ検証 → API 実行 → レスポンスの加工 + エラーハンドリングを一連の処理として記述できます。
非同期処理で try ~ catch 構文を利用すると、どうしても複雑なコードになってしまいますし、エラーが発生した場合に処理フローを追いかけるだけでも一苦労です。
そんな時は、ぜひ neverthrow を検討してみてはいかがでしょうか?
続編となる記事「neverthrow と fetch と zod を組み合わせた非同期処理【直列実行と並列実行編】」を執筆しました。良ければコチラもご覧ください。
Gaji-Labo では「手ざわりのよいUI」の実現を、デザイナーとエンジニアが一緒になって目指しています。
カジュアル面談の場も用意しておりますので、「UI コンポーネントの設計・実装が得意!」という方も「業務ロジックの実装が得意!」という方も、ぜひお気軽にご連絡ください!
Gaji-Labo フロントエンドエンジニア向けご案内資料
Gaji-Labo は Next.js, React, TypeScript 開発の実績と知見があります
フロントエンド開発の専門家である私たちが御社の開発チームに入ることで、バックエンドも含めた全体の開発効率が上がります。
「既存のサイトを Next.js に移行したい」
「人手が足りず信頼できるエンジニアを探している」
「自分たちで手を付けてみたがいまいち上手くいかない」
フロントエンド開発に関わるお困りごとがあれば、まずは一度お気軽に Gaji-Labo にご相談ください。
オンラインでのヒアリングとフルリモートでのプロセス支援にも対応しています。
Next.js, React, TypeScript の相談をする!