Next.js で REST API をつくっていたところ、レスポンスのデータ型を何度も繰り返し書いていることに気づきました。コードを DRY にするために何か良い方法はないかと考えたので、記事としてまとめます。
問題のコード
レスポンスのデータ型は至るところで活躍します。例えば、MSW を用いたモックの定義や、SWR によるデータ取得のコードで散見されました。
/pages/api/members.ts
import { getAllMembers } from '@/lib/members';
import type { Member } from '@prisma/client';
import type { NextApiRequest, NextApiResponse } from 'next';
const handler = async (
request: NextApiRequest,
response: NextApiResponse<{ members: Member[] }>,
// ^^^^^^ 共有したいレスポンスのデータ型
) => {
if (request.method === 'GET') {
const members = await getAllMembers();
response.json({ members });
}
};
export default handler;
/mocks/resolvers/members.ts
const get: ResponseResolver<
RestRequest<never, PathParams<string>>,
RestContext
> = (_, response, context) => {
return response(
context.status(200),
context.json<{ members: Member[] }>({ members }),
// ^^^^^^ モック用に使ったり...
);
};
/components/Members/index.tsx
const Members = () => {
const { data, isLoading } = useSWR<{ members: Member[] }>(
// ^^^^^^ データ取得に使ったりする
'/api/members',
(url: string) =>
axios.get<{ members: Member[] }>(url).then((response) => response.data),
// ^^^^^^ ここでも...
);
// return <>...</>
繰り返しを排除する
API の型定義からデータ型を取り出せばよいと考え、次のようなユーティリティを作成しました。
/types/utils.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export type ResponseData<T> = T extends (
request: NextApiRequest,
response: NextApiResponse<infer U>,
) => void | Promise<void>
? U
: never;
Generic で API の型を受け取り、Conditional Type と infer の組み合わせでレスポンスのデータ型を取り出しています。呼び出し側のコードは以下のように書き換えられます。
/mocks/resolvers/members.ts
diff --git a/src/mocks/resolvers/members.ts b/src/mocks/resolvers/members.ts
index 84c4029..9723d99 100644
--- a/src/mocks/resolvers/members.ts
+++ b/src/mocks/resolvers/members.ts
@@ -1,6 +1,7 @@
import { members } from '../fakeData/members';
-import type { Member } from '@prisma/client';
+import type handler from '@/pages/api/members';
+import type { ResponseData } from '@/types/utils';
import type {
PathParams,
ResponseResolver,
@@ -14,7 +15,7 @@ const get: ResponseResolver<
> = (_, response, context) => {
return response(
context.status(200),
- context.json<{ members: Member[] }>({ members }),
+ context.json<ResponseData<typeof handler>>({ members }),
);
};
$
これでデータ型が必要なコードにおいて、同じ型定義を毎回書く必要はなくなりました。また、API エンドポイントが増えた場合も同じようにユーティリティを再利用できます。
おわりに
記事を書いてから考え直したのですが、このユーティリティは果たして使いやすいのでしょうか・・・。公式ドキュメントのサンプルコードにあるとおり、愚直に API の近くでレスポンスのデータ型を定義した方がよいのかもしれません。今は結論を出せないので、よいプラクティスがあれば取り入れていきたいです。