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'",
},
]