概要
外部 API のレスポンスは unknown 型として扱うのが安全。
Zod の safeParse を使うと、スキーマ不一致時に例外を throw せずに success / error で分岐できる。
型定義と検証ロジックを Zod スキーマで一元管理できるため、型キャストが不要になる。
インストール
npm install zod
実装例
// src/lib/schemas/post.ts
import { z } from "zod";
export const PostSchema = z.object({
id: z.number(),
title: z.string(),
body: z.string(),
userId: z.number(),
});
export const PostListSchema = z.array(PostSchema);
export type Post = z.infer<typeof PostSchema>;
// src/lib/api/posts.ts
import { PostListSchema, type Post } from "@/lib/schemas/post";
type FetchResult<T> =
| { success: true; data: T }
| { success: false; error: string };
export async function fetchPosts(): Promise<FetchResult<Post[]>> {
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
if (!res.ok) {
return { success: false, error: `HTTP error: ${res.status}` };
}
const json: unknown = await res.json();
const result = PostListSchema.safeParse(json);
if (!result.success) {
return { success: false, error: result.error.message };
}
return { success: true, data: result.data };
}
// src/app/posts/page.tsx
import { fetchPosts } from "@/lib/api/posts";
export default async function PostsPage() {
const result = await fetchPosts();
if (!result.success) {
return <p className="text-red-600">エラー: {result.error}</p>;
}
return (
<ul className="space-y-2">
{result.data.map((post) => (
<li key={post.id} className="rounded border px-4 py-3 text-sm">
<p className="font-medium">{post.title}</p>
<p className="text-gray-500">{post.body}</p>
</li>
))}
</ul>
);
}
ポイント
safeParseは例外を throw しないため、try/catch なしでsuccessフラグで分岐できるz.infer<typeof PostSchema>でスキーマから型を自動導出し、型定義の重複をなくすunknownとして受け取ってからsafeParseに通すことで、実行時の型安全を確保できる- スキーマを
src/lib/schemas/に分離すると、フォームバリデーション(zod + react-hook-form)と同じスキーマを再利用できる