Zenn
😋

【TS】APIの取得後の型どうしてる??

に公開2

はじめに

どうも、お久しぶりです。てるし〜です。
4月よりITエンジニアとなってから3年が経ちました。
もう、新人だからとか言い訳ができないような年になってしまったので、気を引き締めて頑張っていこうと思います。

さてさて、今日のお題はAPIを叩いて取得して値の型についてを書いていこうかと思います。

私自身、その部分の型定義をどうすべきかという疑問を抱き考えるようになりました。
まだ、本当にこれで良いのか?これが最適なのか?というのを疑問に抱いているところではありますが、一旦考えを書いてみようと思います。

前提として

今回は、みんな大好きハリーポッタAPIを例に説明していきたいと思います。

https://hp-api.onrender.com/

こいつをfetch関数を用いてAPIを取得してその後の処理についての一例を書いていこうかなと思います。

レスポンスが正常の時に返ってくるデータの方は以下のとおりです。

{
    id:string,
    name:string;
    alternate_names:string[]
    species:string;
    gender:"male"|"female"|"";
    house:string;
    dateOfBirth:string|null;
    yearOfBirth:number|null;
    wizard:boolean;
    ancestry:string;
    eyeColour:string;
    hairColour:string;
    wand: {
        wood:string;
        core:string;
        length:number|null;
    }),
    patronus:string;
    hogwartsStudent:boolean;
    hogwartsStaff:boolean;
    actor:string;
    alternate_actors:string[];
    alive:boolean;
    image:string();
}

ざっくりとした処理のフロー

簡単に処理のフローを書いてみます。

こうやっている人いない???

今から下に一例のコードを書きます。

import { CustomError } from "@/lib/services/custom-error";
import { ErrorHandler } from "@/lib/services/error-handler";
import { appConfig } from "@/shared/config";
import { APIRes, APIScheme } from "./response.type";
import { ParseRes } from "./parse";
import { createResult, Result } from "@/lib/core/result";
import { UsePotter } from "./mock-api.type";

export async function getCharacter(): Promise<
    Result<UsePotter[], CustomError>
> {
    const res = await fetch(appConfig.api);

    if (!res.ok) {
        return ErrorHandler({ status: res.status });
    }

    const body = await res.json() as APIRes[];

    const parseBody = ParseRes(body);

    return createResult.ok(parseBody);
}

結構こうやって書いている人は多いのかなと勝手に思ってます(違うぞという人はごめんさい🙇🏻‍♂️)。
個人的にこれは良いのか?と思う部分は

const body = await res.json() as APIRes[];

です。

asを使うのは確かに一つの手段ではあるとは思いますが、APIの型が急に変わったであったり多くのスキーマがあってミスをしていることに気づかないということがあるとJavascriptやTypescriptはその辺は実行するまでエラーを吐かなかったりバグになることが多いので「安全であるか?」と言われればそうでないかなと思います。

では、どうすればいいのでしょうか??

順番に見ていきましょう。

正式な処理のフロー

まずは、修正した処理のフローを書いていこうと思います。

では型定義をどうするか

いくつかパターンがあり、

  1. isを使う
  2. zodなどのライブラリを使う
    などなどです。

1. isを使う

https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard#yznotype-guard

GPTの発言を引用すると

「ある値が特定の型かどうかを判断して、以降のコードでその型として扱えるようにする」ために使います。

要はtypeof等で書いた条件をクリアすればTSが勝手に型を絞ってくれるので型推論ができるようになります。

function isAPIRes(value:unknown) value is APIRes{
    return typeof id==="string"&&
           typeof name==="string"&&
           .....
}

とはいえ、、、、
これを人間の手でやっていくのかと。。。。う〜ん、、正直やりたくありません。

2. zodのようなライブラリを使う

https://zod.dev/

zodのようなライブラリを使って型ガードはできます(一応)。

私はめんどくさがりなので、今回はzodを使って型検証をしていきたいと思います。

zodを使って型検証

1. 型のスキーマ作り

今回zodを使うのでその型定義のスキーマを作っていきます(正直めんどかったw)。

import { z } from "zod";
export const APIScheme = z.array(
    z.object({
        id: z.string(),
        name: z.string(),
        alternate_names: z.array(z.string()),
        species: z.string(),
        gender: z.enum(["male", "female", ""]),
        house: z.string(),
        dateOfBirth: z.string().nullable(),
        yearOfBirth: z.number().nullable(),
        wizard: z.boolean(),
        ancestry: z.string(),
        eyeColour: z.string(),
        hairColour: z.string(),
        wand: z.object({
            wood: z.string(),
            core: z.string(),
            length: z.number().nullable()
        }),
        patronus: z.string(),
        hogwartsStudent: z.boolean(),
        hogwartsStaff: z.boolean(),
        actor: z.string(),
        alternate_actors: z.array(z.string()),
        alive: z.boolean(),
        image: z.string()
    })
);

export type APIRes = z.infer<typeof APIScheme>;

ざっと上記のような感じです。
APIResはスキーマを元に型を作成しています。

2. スキームを利用して、型チェックをする

zodにはスキームを作った場合にsafeParseというメソッドが存在します。

https://zod.dev/?id=safeparse

フローとしては、

といったイメージになるはずです。

似たようなものでparseというメソッドが存在します。
https://zod.dev/?id=parse

こいつはエラーをthrowします。
この判断はプロジェクトによって違ってくるんではないでしょうか?

エラーの形をカスタムした上でthrowしたいであったり、普通にreturnしたいとかであればsafeParseが良いかと思います。

実際に変更したものが下記コードになります。

import { CustomError } from "@/lib/services/custom-error";
import { ErrorHandler } from "@/lib/services/error-handler";
import { appConfig } from "@/shared/config";
import { APIScheme } from "./response.type";
import { ParseRes } from "./parse";
import { createResult, Result } from "@/lib/core/result";
import { UsePotter } from "./mock-api.type";

export async function getCharacter(): Promise<
    Result<UsePotter[], CustomError>
> {
    const res = await fetch(appConfig.api);

    if (!res.ok) {
        return ErrorHandler({ status: res.status });
    }

    const body = await res.json();

    const typeResult = APIScheme.safeParse(body);

    if (!typeResult.success) {
        //後で詳細のエラーを作る
        throw new Error("API型が違います");
    }

    const parseBody = ParseRes(typeResult.data);

    return createResult.ok(parseBody);
}
    const typeResult = APIScheme.safeParse(body);

    if (!typeResult.success) {
        //後で詳細のエラーを作る
        throw new Error("API型が違います");
    }

    const parseBody = ParseRes(typeResult.data);

この部分で型チェックをしてダメならエラーを投げる、あってたら次のステップにという形のコードにしています。

型チェックをすることで何が良くなるのか?

asでやる場合より型チェックをすることで何が良くなるのかですがエラーを吐いてくれるのでミスに気付きやすい、もしくはAPIのスキーム変更に気付きやすいです。

私の最近投稿している記事を見ていればいくつか「エラーは良いぞ」的なことを話していると思いますが、それがここでも恩恵を受けることになります。

型チェックが失敗したらエラーを吐いてくれるのです。

逆にasを使うと型定義をミスしていた場合等にエラーが吐かれずそのメソッドが存在せずundefinedとなってしまい何がどうなっているのかが分からなずパニック状態になることもあります。

対処が慣れているとはいえそのような工数は削減したいものです。

さらにAPIのスキームの変更された場合も気付くことができます。
まぁ〜普通に報告をしてくれ、とは思いますが通達できてない時でもエラーによってバグを起こす前に気付くことができます。

やっぱりエラーってありがたいものなのですよ!

まとめ

fetch等でAPIを取得した後の型定義については、isを使って型ガードをするかzod等のライブラリを使ってガードするかをした方が個人的には良いのではないかと思っています。

asでやるのは安全な開発を目指す上でよろしくないですし、anyなんて論外ですね。。。

もし、上記の作業をしていなかったという人は型チェックの工程を入れると良いかなと思います。

最後に

いかがだったでしょうか?
型定義について書いてきましたが私自身もまだこれで良いのかな?という部分があります。
また良いものが出てきたら記事にしようかなと思います。

TSはきちんと型定義やチェックを自らしないとバグ量産や下手したら脆弱性なコードが量産されてしまうことがあります。

ですが、結構人の手でやるのってしんどくないか???って部分が多く言語単位でやってくれても良いことをライブラリでやっていることもあるので結構最近TSは。。。ってなってきています。

っていうのは置いておいて今回も読んでいただきありがとうございました!!!!

Discussion

ryoppippiryoppippi

こんにちは!
それこそtypiaを使ってみるのはどうですか?

てるし〜てるし〜

こんにちは!
それこそtypiaを使ってみるのはどうですか?

こんにちは。はじめまして。
コメントありがとうございます!

typia初めて聞いたのでドキュメントを見に行きましたが第一印象は良い感じです!
使って検証してみたいと思います。
form処理でのresolverはないようですがしっかり型チェックしたいといった場合には良さそうですね。
(パッとみた感想なので間違っているかもですが)

貴重なアドバイスありがとうございます!

ログインするとコメントできます