最近関わっているReactアプリでは良くあるようにAPIサーバーと通信しています。アプリの開発が進み扱うAPIの数も増えて来ると、APIが戻すエラーコードの処理で困るようになってきました。
エラーの種類によっては処理や表示を変えないといけませんが、初期のコードではAPIサーバーのコードを読まないとアプリのエラー処理が書けなくなっていました。TypeScriptを使っていてもエラーの型がnumber型では、どのようなエラーが戻ってくるのかはわかりません。
初期のコード
Reactアプリを作った事のある方には説明無しでも判るようなコードだと思いますが、簡単な解説を書きます。
- ① エラーコードの名前と値の定義
- ② 認証APIの戻り値(JSON)の型
- 成功の場合は、
status
はok
になり、認証されたユーザーIDがuserId
に戻る - 失敗の場合は、
status
はerror
になり、エラーコードがcode
に戻る
- 成功の場合は、
- ③ アプリから呼び出されるsignIn関数の戻り値の型
- 成功なら
userID
が入り、error
はnullになる - 失敗なら
userID
はnullになり、error
にはエラーコードが入る
- 成功なら
- ④ アプリから呼び出されるsignIn関数
- fetch関数でAPIサーバーに
email
,password
をPOSTし、戻り値を受け取る status
がok
なら成功時の値(userId)が戻るstatus
がerror
なら失敗時の値(error)が戻る
- fetch関数でAPIサーバーに
- ⑤ ユーザー登録API戻り値(JSON)の型
- 成功の場合は、
status
はok
になる - 失敗の場合は、
status
はerror
になり、エラーコードがcode
に戻る
- 成功の場合は、
- ⑥ アプリから呼び出されるsignUp関数の戻り値の型
- 成功なら
error
はnullになる - 失敗なら
error
にはエラーコードが入る
- 成功なら
- ⑦ アプリから呼び出されるsignUp関数
- fetch関数でAPIサーバーに
email
,password
をPOSTし、戻り値を受け取る status
がok
ならnullが戻るstatus
がerror
ならエラーコードが戻る
- fetch関数でAPIサーバーに
- ⑧ 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バンザーイ❗