zukucode
主にWEB関連の情報を技術メモとして発信しています。

AjvでAPIのレスポンスの型チェックを行う

TypeScript fetchの共通処理を実装するfetchの共通処理を作成しましたが、これはAPIからのレスポンスの形式があっている前提で処理を行っています。

例えば以下はAPIのレスポンスの型はBook型である前提となっています。

type Book = {
  id: number;
  title: string;
};

export const getBookById = async (
  param: { id: number },
): Promise<ApiResult<Book>> => {
  const response = await post('/api/books/findById', param);
  return response.ok ? { ...response, data: response.data as Book } : response;
};

例えば、APIのレスポンスが以下ではなく、

{
  id: number;
  title: string;
}

以下だった場合を考えます。

{
  id: string;
  name: string;
}

以下の処理はTypeScriptのビルドは通りますが、実行時にエラーが発生してしまいます。

// titleはundefinedのためエラーになる
const title = data.title.toUpperCase();

TypeScriptで実行時エラーが発生するケースとして、response.data as Bookのように、asで型変換を行っている場合が多いです。

asを使わずに、APIのレスポンスが本当に想定している型かどうか、チェックを行い、チェックがOKの場合はその型に型変換し、NGの場合は例外とする処理を実装します。

Ajvというライブラリを使用した型チェックの方法を紹介します。

パッケージのインストール

まずはパッケージをインストールします。

$ npm install ajv

型スキーマの作成

以下のように、型スキーマの変数を作成します。

const bookSchema = {
  properties: {
    id: { type: 'int32' },
    title: { type: 'string' },
  },
} as const;

文字列はstring、数値はint32などを指定します。

詳しくは以下の公式ドキュメントを参考にしてください。

https://ajv.js.org/json-type-definition.html

型スキーマから以下のようにして、型を取得できます。

import { JTDDataType } from "ajv/dist/jtd";

type Book = JTDDataType<typeof bookSchema>;

以下のようなチェック処理を作成します。

パラメータのschemaには、先ほど定義したbookSchemaなどの型スキーマを指定し、dataにはレスポンスデータを指定します。

この時点ではレスポンスデータの型はわからないので、unknownとなっています。

型チェックがOKの場合は、レスポンスデータを、型スキーマから作成した型(bookSchemaから作成したBook型)として返却します。

※わかりやすく言い換えると、型チェックと型変換を両方行うファンクションです。

export const getValidatedData = <T extends AnySchema>(schema: T, data: unknown): JTDDataType<T> => {
  // 型チェックを行うファンクションを生成
  const validate = new Ajv({
    allErrors: false,
  }).compile<JTDDataType<T>>(schema);

  // 型チェックを行う
  if (validate(data)) {
    // OKの場合はその型としてレスポンスデータを返す
    return data;
  } else {
    // NGの場合はエラー
    throw new Error(JSON.stringify(validate.errors));
  }
};

使い方

以下のように、asで変換していた部分を、getValidatedDataを使用するように修正します。

export const getBookById = async (
  param: { id: number },
): Promise<ApiResult<Book>> => {
  const response = await post('/api/books/findById', param);
  return response.ok ? { ...response, data: response.data as Book } : response;
  return response.ok ? { ...response, data: getValidatedData(bookSchema, response.data) } : response;
};

動作確認

試しに、APIからのレスポンスをtitleではなくnameに変更してみます。

すると型チェックのファンクションで以下のエラーが発生することが確認できます。

[
  {
    instancePath: '',
    schemaPath: '/properties/title',
    keyword: 'properties',
    params: { error: 'missing', missingProperty: 'title' },
    message: "must have property 'title'",
  },
]

関連記事