🐶

RemixのエラーハンドリングでTypeScriptの型をあてたい

2024/03/16に公開

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
https://remix.run/docs/en/main/guides/errors

Remixドキュメント > Throwing Response in Loaders
https://remix.run/docs/en/main/route/loader#throwing-responses-in-loaders

mdn web docs > Responseオブジェクト
https://developer.mozilla.org/ja/docs/Web/API/Response

Discussion