BETA

TypeScriptでfetch処理

投稿日:2020-06-24
最終更新:2020-06-26

fetch処理を型安全に書く方法について解説を書きます。

fetchProfilemainのメソッドで、プロフィールを取得する流れを例に解説します。

ベースのコード

mainで、fetchProfileを実行して値を取得します。よくある流れですが何点か課題があります。

type Profile = {  
 id: number;  
 name: string;  
 foo: string;  
 bar: string;  
};  


const fetchProfile = () => {  
 return fetch("/profile").then<Profile>((res) => res.json());  
};  

const main = async () => {  
 let profile: Profile | undefined;  
 try {  
   profile = await fetchProfile();  
 } catch (e) {  
   // エラーハンドリング  
 }  

 if (profile) {  
   console.log(profile.name, "さん");  
 }  
};  

main内でletを使用している点

letは避けたいです。コードリードする際も値が変わるのか?と思いながら見る必要があります。
しかし、tryの中でconstを使うと、profileの変数が無い可能性があるためエラーになります。(下記のような状態です)

サンプルコード

const main = async () => {  
 try {  
   const profile = await fetchProfile();  
 } catch (e) {  
   // エラーハンドリング  
 }  

 if (profile) { // ここでエラーになる  
   console.log(profile.name, "さん");  
 }  
};  

fetchProfileのエラーが考慮されていない点

fetchProfileの実行結果がPromiseになっています。しかし実際は必ず成功する訳ではありません。
実装を見る必要があり、TypeScriptのメリットが減ってしまいます。

課題

  • fetchProfileは、コードを見るまでエラーを返すかわからない。
  • letを使い見苦しいコードになっている点です。

エラー時の型情報が知りたい

先程のコードにtry-catchを追加しました。throw new Error(e);も追加しています。しかしこのコードでもエラー時の型は不明です。

サンプルコードのリンク先の、fetchProfileの関数をマウスオーバーで確認してみてください。

サンプルコード

type Profile = {  
 id: number;  
 name: string;  
 foo: string;  
 bar: string;  
};  

const fetchProfile = () => {  
 try {  
   return fetch("/profile").then<Profile>((res) => res.json());  
 } catch (e) {  
   throw new Error(e);  
 }  
};  

ちいさな修正をする事でError情報の型を取得できます。
return new Error(e); に変更しています。こうする事で型情報がわかるようになります。

サンプルコード

type Profile = {  
 id: number;  
 name: string;  
 foo: string;  
 bar: string;  
};  

const fetchProfile = () => {  
 try {  
   return fetch("/profile").then<Profile>((res) => res.json());  
 } catch (e) {  
   return new Error(e);  
 }  
};  

letをやめる

fetchProfileがエラーを考慮した実装となり、呼び出し元のmainはtry-catchを外すことできます。
また実行結果も正確な型情報を得ることができます。
しかしまだ改善の余地はあります。

type Profile = {  
  id: number;  
  name: string;  
  foo: string;  
  bar: string;  
};  

const fetchProfile = () => {  
try {  
  return fetch("/profile").then<Profile>((res) => res.json());  
} catch (e) {  
  return new Error(e);  
}  
};  

const main = async () => {  
 const profile = await fetchProfile();  
 if (profile instanceof Error) {  
   // エラーハンドリング  
   return;  
 }  
 console.log(profile.name, "さん");  
};  

さらに追求

  • instanceof Errorのfalseが本当に成功なのか?
  • try-catch無くして大丈夫なのか?
  • Error時にPromiseがはずれていて扱いづらい

コードを整理すると隠れていいた課題が見えてきます。このあたりも対応していきます。

fetchProfileを改善

処理の成功or失敗がをわかりやすくするために、Dataのclassで包みます。
さらにtry-catchからthen-catchに変更しPromiseである型情報もしっかり残します。

type Profile = {  
 id: number;  
 name: string;  
 foo: string;  
 bar: string;  
};  

abstract class Data<T> {  
 protected constructor(private _value: T) {}  

 get value(): T {  
   return { ...this._value } as const;  
 }  
}  
class SuccessData<T> extends Data<T> {  
 static of<V>(v: V): SuccessData<V> {  
   return new SuccessData(v);  
 }  
}  

class ExceptionData<T> extends Data<T> {  
 static of<V>(v: V): ExceptionData<V> {  
   return new ExceptionData(v);  
 }  
}  

const fetchProfile = () => {  
 return fetch("/profile")  
   .then<Profile>((res) => res.json())  
   .then((res) => SuccessData.of(res))  
   .catch((e) => ExceptionData.of(e));  
};  

mainを改善

下記のように、catchのみを付与してfetchProfileを呼び出しています。
この書き方の場合、catchにジェネリクスでneverを付与しないとanyになります。(呼び出し元で例外対応していますのでcatchに入る可能性はかなり低いです。)

成功の場合、失敗の場合とタイプガードできていますが、例外のエラーもありえます。
ここまで到達するとthrow new Error("例外発生");をしても良いと思います。

サンプルコード

const main = async () => {  
 const res = await fetchProfile().catch<never>((e) => e);  
 if (res instanceof SuccessData) {  
   const profile = res.value;  
   console.log(profile.name, "さん");  
   return;  
 }  

 if (res instanceof ExceptionData) {  
   // エラーハンドリング  
   return;  
 }  

 throw new Error("例外発生");  
};  

instanceofを利用して成功と失敗を判断可能になりました。データの中身に依存しない判断なので、APIの戻り値が変わっても耐えられます。
簡易に書く場合は、下記のようなisErrを付与してタイプガードしても良いです。

const res = {isErr: true, data: '...'}  

まとめ

例外を投げずにハンドリングする事で、型安全に開発がすすめられます。

補足事項

  • SuccessDataとExceptionData用のclassは、fp-tsのようなライブラリを使うのも良いと思います。
  • ExceptionDataに関しては、Error classを継承するのも良いと思います。
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@tommyの技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう