RemixのエラーハンドリングでTypeScriptの型をあてたい
RemixでWebアプリ開発していて、TypeScriptのエラー対処でつまづきました...
今後のために対応策を記事にしています。
イシュー
ログイン時のサーバー側の処理を書いている際に、以下のような赤波線に遭遇しました。
赤波線をマウスオーバーすると以下のように表示されます。
Property 'status' does not exist on type 'Error'.ts(2339)
TypeScriptのエラーですね。。
Errorオブジェクトのプロパティにstutusは無いそうです。。
サーバー側の処理でstatusを指定してErrorをthrowするために、このTSの赤波線をなんとかしたいです。
Remixドキュメントの Error Handlingを読んでみましたが、サーバー側のそれっぽいコードは無さそうだったので個人的な対応策を考えてみました。
対応策1:Errorオブジェクトの拡張
server側
Errorオブジェクトを拡張してプロパティにstatusを加えることにしました。
以下のようなErrorを拡張したクラスを作成します。
export default class ServerError extends Error {
status: number;
constructor(message: string, status: number = 500) {
super(message);
this.status = status;
}
}
これを使って先ほどのコードを書き換えます。
// before
if (!existingUser) {
const error = new Error(ErrorMessageEnum.LoginFailedByEmail);
error.status = 401;
throw error;
}
// after
if (!existingUser) {
throw new ServerError(ErrorMessageEnum.LoginFailedByEmail, 401);
}
これで赤波線は消えました。3行が1行で書けるようにもなったので良い感じです!
route側
クライアント側では、throwされたError statusを元にメッセージの出し分けを行います。
Remixのrotues配下ではaction関数を使ってサーバー側の処理を実行しますが、その際にError statusを元に返却する値を分岐させています。
以下の場合を想定。
- statusが422or401の場合はエラーメッセージをクライアントへ送る
- それ以外はコンソール出力だけしてメッセージを返さない
export async function action({ request }: ActionFunctionArgs) {
// 途中コードは省略
try {
if (authMode === "login") {
return await login({ email, password });
} else {
return await signup({ email, password });
}
} catch (error) {
if (error instanceof ServerError && [422, 401].includes(error.status)) {
return error.message;
}
console.error(error);
return null;
}
}
クライアント側では、Remixで用意されているHooksのuseActionDataを使ってメッセージを取得できます。messageが有れば表示、無ければ何も表示しないようにしています。
import { Form, useActionData } from "@remix-run/react";
export default function AuthForm() {
const message = useActionData<string>();
// 途中コードは省略
return (
<Form>
// 途中コードは省略
{message && <p>{message}</p>}
</Form>
);
}
対応策2:Responseオブジェクトを利用
server側
Remixドキュメントの Throwing Responses in Loaders に使えそうなコードがありました。
Responseオブジェクトをthrowする際にstatusを指定できるようです。
これを利用してエラーの部分を書き換えます。
// before
if (!existingUser) {
const error = new Error(ErrorMessageEnum.LoginFailedByEmail);
error.status = 401;
throw error;
}
// after
if (!existingUser) {
throw new Response(ErrorMessageEnum.LoginFailedByEmail, 401);
}
これで赤波線は消えました。
route側
同様にstatusの値で表示内容を分岐させます。
Responseオブジェクトの場合はメッセージを取得するために text()
を使います。このメソッドはPromiseを返すのでawaitを付ける必要があります。
export async function action({ request }: ActionFunctionArgs) {
// 途中コードは省略
try {
if (authMode === "login") {
return await login({ email, password });
} else {
return await signup({ email, password });
}
} catch (error) {
if (error instanceof Response && [422, 401].includes(error.status)) {
return await error.text();
}
console.error(error);
return null;
}
}
クライアント側では、同様useActionDataを使ってメッセージを取得できます。
参考
Remixドキュメント > Error Handling
Remixドキュメント > Throwing Response in Loaders
mdn web docs > Responseオブジェクト
Discussion