EY-Office ブログ

エラーコードもAPI毎に正確に型定義しよう

最近関わっているReactアプリでは良くあるようにAPIサーバーと通信しています。アプリの開発が進み扱うAPIの数も増えて来ると、APIが戻すエラーコードの処理で困るようになってきました。

エラーの種類によっては処理や表示を変えないといけませんが、初期のコードではAPIサーバーのコードを読まないとアプリのエラー処理が書けなくなっていました。TypeScriptを使っていてもエラーの型がnumber型では、どのようなエラーが戻ってくるのかはわかりません。

TypeScript

初期のコード

Reactアプリを作った事のある方には説明無しでも判るようなコードだと思いますが、簡単な解説を書きます。

  • ① エラーコードの名前と値の定義
  • ② 認証APIの戻り値(JSON)の型
    • 成功の場合は、statusokになり、認証されたユーザーIDがuserIdに戻る
    • 失敗の場合は、statuserrorになり、エラーコードがcodeに戻る
  • ③ アプリから呼び出されるsignIn関数の戻り値の型
    • 成功ならuserIDが入り、errorはnullになる
    • 失敗ならuserIDはnullになり、errorにはエラーコードが入る
  • ④ アプリから呼び出されるsignIn関数
    • fetch関数でAPIサーバーにemail, passwordをPOSTし、戻り値を受け取る
    • statusokなら成功時の値(userId)が戻る
    • statuserrorなら失敗時の値(error)が戻る
  • ⑤ ユーザー登録API戻り値(JSON)の型
    • 成功の場合は、statusokになる
    • 失敗の場合は、statuserrorになり、エラーコードがcodeに戻る
  • ⑥ アプリから呼び出されるsignUp関数の戻り値の型
    • 成功ならerrorはnullになる
    • 失敗ならerrorにはエラーコードが入る
  • ⑦ アプリから呼び出されるsignUp関数
    • fetch関数でAPIサーバーにemail, passwordをPOSTし、戻り値を受け取る
    • statusokならnullが戻る
    • statuserrorならエラーコードが戻る
  • ⑧ signIn関数を呼び出すサンプルコード
const SERVER_URL = "https://server.com/";
const headers = { "Content-Type": "application/json" };

const ApiError = {                                              // ← ①
  System: -100,
  Validation: -200,
  AuthorizeFailed: -201,
  AlreadyRegistered: -202,
} as const;

type ApiSignInBodyType = {                                      // ← ②
  status: "ok",
  userId: number
} | {
  status: "error",
  code: number
};
type SignInReturnType = {                                       // ← ③
  userId: number | null;
  error: number | null;
}

export const signIn = async (email: string, password: string):  // ← ④
     Promise<SignInReturnType> => {
  const response = await fetch(`${SERVER_URL}/sign_in}`,
    { method: "POST", headers, body: JSON.stringify({email, password }) });
  const result: ApiSignInBodyType = await response.json();
  if (result.status === "ok") {
    return {userId: result.userId, error: null};
  } else {
    return {userId: null, error: result.code};
  }
}

type ApiSignUpBodyType = {                                      // ← ⑤
  status: "ok"
} | {
  status: "error",
  code: number
};
type SignUpReturnType = {                                       // ← ⑥
  error: number | null;
};

export const signUp = async (email: string, password: string):  // ← ⑦
    Promise<SignUpReturnType> => {
  const response = await fetch(`${SERVER_URL}/sign_up}`,
    { method: "POST", headers, body: JSON.stringify({email, password }) });
  const result: ApiSignUpBodyType = await response.json();
  if (result.status === "ok") {
    return {error: null};
  } else {
    return {error: result.code};
  }
}

export const main = async () => {                               // ← ⑧
  const {userId, error} = await signIn("user1@test.com", "password1");
  if (error == ApiError.System) {
    console.log("Error...");
  } else if (error === ApiError.Validation) {                   // ← ⑨
    console.log("Error...");
  } else {
    console.log("OK ", userId)
  }
}

実は、⑨のエラーコード比較は間違いです。 認証APIはこのエラーコードを戻しません、しかしコンパイル(VS Codeの型チェック)でも、実行時にもエラーになりません!

書き直したコード

書き直したコードではAPIが戻すエラーコードをTypeScriptの型として定義するようにしました。

  • ⑩ 認証APIが戻すエラーコードの型
  • ⑪ エラーコードは上の型定義を使っています
  • ⑫ アプリから呼び出されるsignIn関数、コードは以前と同じです
  • ⑬ ユーザー登録APIが戻すエラーコードの型
  • ⑭ エラーコードは上の型定義を使っています
const SERVER_URL = "https://server.com/";
const headers = { "Content-Type": "application/json" };

const ApiError = {
  System: -100,
  Validation: -200,
  AuthorizeFailed: -201,
  AlreadyRegistered: -202,
} as const;

type ApiSignInErrors = typeof ApiError.System |
                       typeof ApiError.AuthorizeFailed;         // ← ⑩
type ApiSignInBodyType = {
  status: "ok",
  userId: number
} | {
  status: "error",
  code: ApiSignInErrors                                         // ← ⑪
};
type SignInReturnType = {
  userId: number | null;
  error: ApiSignInErrors | null;                                // ← ⑪
}

export const signIn = async (email: string, password: string):  // ← ⑫
    Promise<SignInReturnType> => {
  const response = await fetch(`${SERVER_URL}/sign_in}`,
    { method: "POST", headers, body: JSON.stringify({email, password }) });
  const result: ApiSignInBodyType = await response.json();
  if (result.status === "ok") {
    return {userId: result.userId, error: null};
  } else {
    return {userId: null, error: result.code};
  }
}

type ApiSignUpErrors = typeof ApiError.System | typeof ApiError.Validation |
                       typeof ApiError.AlreadyRegistered;       // ← ⑬
type ApiSignUpBodyType = {
  status: "ok"
} | {
  status: "error",
  code: ApiSignUpErrors                                         // ← ⑭
};

type SignUpReturnType = {
  error: ApiSignUpErrors | null;                                // ← ⑭
};

export const signUp = async (email: string, password: string):
    Promise<SignUpReturnType> => {
  const response = await fetch(`${SERVER_URL}/sign_up}`,
    { method: "POST", headers, body: JSON.stringify({email, password }) });
  const result: ApiSignUpBodyType = await response.json();
  if (result.status === "ok") {
    return {error: null};
  } else {
    return {error: result.code};
  }
}

export const main = async () => {
  const {userId, error} = await signIn("user1@test.com", "password1");
  if (error == ApiError.System) {
    console.log("Error...");
  } else if (error === ApiError.Validation) {                   // ← ⑮
    console.log("Error...");
  } else {
    console.log("OK ", userId)
  }
}

最初のコードで問題だった⑮は、下の画像のようにVS Codeで型エラーが表示されています。

まとめ

エラーコードの設計はロジック等に比べるとおざなりになりがちです、またコードを書いてからエラーの存在に気付くことも多く、取りあえずmumber型などになりがちです。そのため最初の例のように不要なエラーチェックをしていたり、必要なエラーチェックが抜けてしまったりします。

このような場合には、書き直したコードのように明確なエラー型を定義する事でエラー処理のバグを減らせると思います。TypeScriptバンザーイ❗

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ