TypeScriptではAPIからの戻りデータの型をtypeで指定できます。しかしこれを信用してはいけません。実行時にはこの型情報は失われます。指定したのとは全く違うデータが返り、何もチェックされないままそれを利用しようとしてアプリが盛大にクラッシュすることもあります。
例えば本番環境ではAPIサーバとそれを呼び出すフロントの間にキャッシュサーバを入れる構成はよくあると思います。何らかの理由でキャッシュサーバーがエラー情報を返す(ただし通信自体は成功)と、上記のようなクラッシュの原因になります。検証環境では起こらず本番環境のみで起こる危険な障害の発生です 。
そこでAPIから戻ってきたデータはtypeやinterfaceで受けず、専用クラスのコンストラクタに渡し、データの存在チェックと存在しない場合のデフォルト値の設定をしましょう。必要ならデータが無効であることを示すフラグ等も入れると良いでしょう。またロード中の状態を示すフラグ等も入れておけばプレースホルダを表示するかどうかの判断に利用できます。
// データクラス
class DataClass
{
public str : string = ""
public num : number = 0
public bLoading = true
public bError = false
constructor(data? : any)
{
// データの存在チェック
if(!data){
this.bError = true
return
}
this.str = data?.String ?? ""
this.num = parseInt(data?.Number ?? "0")
// データの整合性チェック
if(!this.check()){
this.bError = true
}
this.bLoading = false
}
public check() : boolean
{
// データの整合性をチェックする。
}
}
// API呼び出しコード
const async API = () => {
const item = await
// fetchなど
.then((rawData : any) => {
const item : DataClass = new DataClass(rawData)
if(item.bError){
// 無視する・例外を投げる・ログに記述するなど
}
return item
}
.catch((err : any)=>{
return new DataClass() // デフォルト値
}
return item // 常にDataClass型
}
// 表示コンポーネント
const Message = ({item}:{item:DataClass}) =>
{
// itemはDataClass型であることが保証されているため
// 存在チェックの必要はない
if(item.bError){
return null
}
if(item.bLoading){
return <div>Loading...</div>
}
return <div>{item.str}</div>
}
こうすることで戻りデータの型が常にDataClassになることが保証されます。
逆にそうしないと戻りデータの型が信用できないため、利用時に?などでチェックする必要が発生してコードが見にくくなります。以下のようにDataTypeにはstrもnumも必須と指定しているにもかかわらず、利用時にはいちいちその存在をチェックしなくてはならないのです。
type DataType = {
str: string,
num: number
}
// 利用時
const item : DataType = await API()
let sum = 0
// エラーコードが返ってきているかもしれないので存在チェック
sum += item.num ?? 0
また通信エラーが発生したらundefinedやnullを返すAPI呼び出しコードもよく見かけますが、問題を悪化させるだけなのでやめましょう。なぜならDataType|undefinedはそれを利用するコードのいたるところに伝染し、利用側に「想定しないデータでないこと」に加えて「undefined/nullでないこと」を確認する義務を負わせてしまうからです。このような場合にも上記のように無効なデータであることを明示したデフォルト値のクラスオブジェクトを返すべきです。
// データを利用する関数
function f(d : DataType){
return d.num+1
}
// 悪の元凶・・・
const item : DataType|undefined = await API()
f(item)// 文法エラー: itemはDataType|undefined型のため
// itemはundefinedかもしれないし、想定しないデータかもしれない。
// だからfが利用する全ての項目をチェックしないと・・・
f(item?.num ?? {str : "", num:0})
// 大量にある呼び出し側でチェックするのは面倒だから
// fの方を変えちゃえ!
function f2(d : DataType|undefined){
return (d?.num ?? 0) + 1 // 存在チェックが伝染・・・
}
// 元からある関数
function f3(d : DataType){
return d.num-1
}
// f2からf3を利用する改修が入った
function f2(d : DataType|undefined){
f3(d) // 文法エラー
f3(d?.num ?? {str : "", num:0}) // ここでも存在チェックが・・・
return (d?.num ?? 0) + 1
}
// こうしてf3にもDataType|undefinedを受け入れる修正が伝染する
function f4(d : DataType|undefined){
return (d?.num ?? 0) - 1 // もちろん存在チェックも伝染
}
// そもそもクラスで受けておけばよかったのだ
function f(d : DataClass){
return d.num+1
}
const item : DataClass = await API()
if(item.bError){
// エラー処理
// デフォルト値の場合には以降のコードが何もしないことが分かっている場合は
// エラーをチェックする必要もない
return
}
f(item) // 問題なし
もちろんキャッシュサーバを始め、全ての中間サーバが常に想定通りのデータを返すように対応するのも費用などが許せば選択肢の一つです。