You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 06:35:18