如何用Zod为TMDB API实现基于media_type的条件类型约束?
解决Zod discriminatedUnion验证错误+TMDB API类型安全定义
错误原因分析
你遇到的"Invalid discriminator value. Expected 'tv' | 'movie' | 'all' | 'person'"错误,本质是Zod的discriminatedUnion要求每个分支的鉴别器字段(这里是media_type)必须是精确的字面量类型,且API返回的media_type值必须在你定义的union分支范围内。大概率是你要么没把person类型加入schema分支,要么输入参数传了all但schema没处理对应返回的多类型场景。
正确的类型定义步骤
1. 定义公共基础字段
先把TMDB返回的公共字段抽出来,避免重复代码:
import { z } from "zod"; // TMDB 媒体项公共字段 const BaseMedia = z.object({ id: z.number(), poster_path: z.string().nullable(), overview: z.string(), vote_average: z.number(), vote_count: z.number(), });
2. 分别定义Movie/TV/Person专属字段
每个类型的media_type必须用字面量类型,确保Zod能正确区分分支:
// 电影类型专属字段 const MovieMedia = BaseMedia.extend({ media_type: z.literal("movie"), title: z.string(), original_title: z.string(), release_date: z.string().nullable(), }); // 剧集类型专属字段 const TvMedia = BaseMedia.extend({ media_type: z.literal("tv"), name: z.string(), original_name: z.string(), first_air_date: z.string().nullable(), origin_country: z.array(z.string()), }); // 人物类型专属字段(如果你的请求允许`all`或`person`参数的话) const PersonMedia = BaseMedia.extend({ media_type: z.literal("person"), name: z.string(), original_name: z.string(), known_for_department: z.string(), });
3. 组合成discriminatedUnion
用media_type作为鉴别器,把所有可能的分支加进去:
// 完整的媒体类型schema const MediaSchema = z.discriminatedUnion("media_type", [ MovieMedia, TvMedia, PersonMedia // 如果不需要人物类型可以删掉,但要确保和请求参数匹配 ]); // 导出TypeScript类型供业务代码使用 export type Media = z.infer<typeof MediaSchema>;
结合tRPC处理请求参数
因为你的media_type是请求输入参数,要确保输入和输出的类型对应:
import { initTRPC } from "@trpc/server"; const t = initTRPC.create(); export const appRouter = t.router({ searchMedia: t.procedure .input(z.object({ // 允许的请求参数类型,要和schema分支对应 media_type: z.union([ z.literal("movie"), z.literal("tv"), z.literal("person"), z.literal("all") ]), query: z.string(), page: z.number().optional().default(1) })) // 输出用schema的数组,因为`all`会返回多种类型的结果 .output(z.array(MediaSchema)) .query(async ({ input }) => { const apiKey = process.env.TMDB_API_KEY; const res = await fetch( `https://api.themoviedb.org/3/search/${input.media_type}?api_key=${apiKey}&query=${encodeURIComponent(input.query)}&page=${input.page}` ); const data = await res.json(); // 用Zod验证返回数据,确保类型安全 return MediaSchema.array().parse(data.results); }) });
类型安全效果
现在在业务代码里使用Media类型时,TypeScript会自动做分支判断:
function renderMedia(media: Media) { if (media.media_type === "movie") { // 这里TypeScript知道media有title、release_date字段 return <div>{media.title} ({media.release_date?.slice(0,4)})</div>; } else if (media.media_type === "tv") { // 这里TypeScript知道media有name、origin_country字段 return <div>{media.name} ({media.origin_country.join(", ")})</div>; } else { // 人物类型的处理 return <div>{media.name}</div>; } }
额外注意事项
- 如果你的业务不需要处理
person类型,记得把PersonMedia从discriminatedUnion里删掉,同时把输入参数的media_type限制为movie|tv,避免多余的分支验证。 - 所有TMDB返回的可选字段(比如
release_date)用.nullable()而不是.optional(),因为API会返回null而非省略该字段。
内容的提问来源于stack exchange,提问作者gibbers
相关产品推荐
相关产品推荐

