Conditional keys based on value of another key with Zod

gib*_*ers 12 rest types typescript zod trpc.io

I'm making a project with the TMDB API and trying to make it super type-safe to reinforce some of the TypeScript stuff I'm learning. I'm using Zod to describe the shape of the data returned by the API.

However, I've noticed that depending on the request parameters, the API can send back data with different keys. Specifically, if the API is sending back data from the "trending" endpoint where data.media_type = "movie" it also has the keys title, original_title, and release_date. But if data.media_type = "tv", those three keys are renamed name, original_name, and first_air_date, respectively, as well as a new key of origin_country being added.

As a result, I described the shape of my data like this:

const mediaType = ["all", "movie", "tv", "person"] as const

const dataShape = z.object({
    page: z.number(),
    results: z.array(z.object({
        adult: z.boolean(),
        backdrop_path: z.string(),
        first_air_date: z.string().optional(),
        release_date: z.string().optional(),
        genre_ids: z.array(z.number()),
        id: z.number(),
        media_type: z.enum(mediaType),
        name: z.string().optional(),
        title: z.string().optional(),
        origin_country: z.array(z.string()).optional(),
        original_language: z.string().default("en"),
        original_name: z.string().optional(),
        original_title: z.string().optional(),
        overview: z.string(),
        popularity: z.number(),
        poster_path: z.string(),
        vote_average: z.number(),
        vote_count: z.number()
    })),
    total_pages: z.number(),
    total_results: z.number()
})
Run Code Online (Sandbox Code Playgroud)

基本上,我已经添加.optional()了每个麻烦的键。显然,这不是很类型安全。有没有办法指定键仅在等于 时才origin_country存在,或者键或都是 a ,但其存在是有条件的?media_typetvnametitlez.string()

可能值得说明的是,media_type也在返回的数据之外指定,特别是在 API 调用的输入中(为了完整性,使用 tRPC 看起来像这样):

import { tmdbRoute } from "../utils"
import { publicProcedure } from "../trpc"

export const getTrending = publicProcedure
    .input(z.object({
        mediaType: z.enum(mediaType).default("all"),
        timeWindow: z.enum(["day", "week"]).default("day")
    }))
    .output(dataShape)
    .query(async ({ input }) => {
        return await fetch(tmdbRoute(`/trending/${input.mediaType}/${input.timeWindow}`))
            .then(res => res.json())
    })
Run Code Online (Sandbox Code Playgroud)

任何帮助表示赞赏!

编辑:自从发布这篇文章以来,我已经了解了 Zod 方法discriminatedUnion(),但如果这是正确的方法,我正在努力实现它。目前有这样的事情:

const indiscriminateDataShape = z.object({
    page: z.number(),
    results: z.array(
        z.object({
            adult: z.boolean(),
            backdrop_path: z.string(),
            genre_ids: z.array(z.number()),
            id: z.number(),
            media_type: z.enum(mediaType),
            original_language: z.string().default("en"),
            overview: z.string(),
            popularity: z.number(),
            poster_path: z.string(),
            vote_average: z.number(),
            vote_count: z.number()
        })
    ),
    total_pages: z.number(),
    total_results: z.number()
})

const dataShape = z.discriminatedUnion('media_type', [
    z.object({
        media_type: z.literal("tv"),
        name: z.string(),
        first_air_date: z.string(),
        original_name: z.string(),
        origin_country: z.array(z.string())
    }).merge(indiscriminateDataShape),
    z.object({
        media_type: z.literal("movie"),
        title: z.string(),
        release_date: z.string(),
        original_title: z.string()
    }).merge(indiscriminateDataShape),
    z.object({
        media_type: z.literal("all")
    }).merge(indiscriminateDataShape),
    z.object({
        media_type: z.literal("person")
    }).merge(indiscriminateDataShape)
])
Run Code Online (Sandbox Code Playgroud)

使用上述代码发出具有任意值的请求media_type会记录错误"Invalid discriminator value. Expected 'tv' | 'movie' | 'all' | 'person'"

Luc*_*njo 26

这是使用 Zod 验证模式的一个很好的例子。正如您所注意到的,受歧视的联合是您问题的解决方案,但我认为这是对您上次实现中的 API 模式的误解。

向TMDB API发出一些请求,最基本的模式是这样的:

const schema = {
  page: 1,
  results: [],
  total_pages: 100,
  total_results: 200,
}
Run Code Online (Sandbox Code Playgroud)

因此,在您的 Zod 模式中,您需要首先考虑这一点。之后,我们将使用属性z.discriminatedUnion()内的函数results。baseShape我还在考虑最后一步中的合并或扩展(在 discriminatedUnion 之后)。

const baseShape = z.object({
  adult: z.boolean(),
  backdrop_path: z.string(),
  genre_ids: z.array(z.number()),
  id: z.number(),
  original_language: z.string().default('en'),
  overview: z.string(),
  popularity: z.number(),
  poster_path: z.string(),
  vote_average: z.number(),
  vote_count: z.number(),
});

const resultShape = z
  .discriminatedUnion('media_type', [
    // tv shape
    z.object({
      media_type: z.literal('tv'),
      name: z.string(),
      first_air_date: z.string(),
      original_name: z.string(),
      origin_country: z.array(z.string()),
    }),

    // movie shape
    z.object({
      media_type: z.literal('movie'),
      title: z.string(),
      release_date: z.string(),
      original_title: z.string(),
    }),

    // all shape
    z.object({
      media_type: z.literal('all'),
    }),
  ])
  .and(baseShape);

const requestShape = z.object({
  page: z.number(),
  results: z.array(resultShape),
  total_pages: z.number(),
  total_results: z.number(),
});
Run Code Online (Sandbox Code Playgroud)

您可以在StackBlitz中查看完整的实现以及一些要测试的数据。

  • 你是山羊!感谢您花时间如此彻底地回答 (4认同)