繁体   English   中英

基于 Zod 另一个键值的条件键

[英]Conditional keys based on value of another key with Zod

我正在用 TMDB API 做一个项目,并试图让它超级类型安全,以加强我正在学习的一些 TypeScript 的东西。 我正在使用 Zod 来描述 API 返回的数据的形状。

但是,我注意到根据请求参数,API 可以使用不同的密钥发回数据。 具体来说,如果 API 从data.media_type = "movie"的“趋势”端点发回数据,它还有键titleoriginal_titlerelease_date 但是如果data.media_type = "tv" ,这三个键分别被重命名为nameoriginal_namefirst_air_date ,并添加了一个新的origin_country键。

因此,我这样描述了数据的形状:

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()
})

基本上,我已将.optional()添加到每个麻烦的键中。 显然,这不是非常类型安全的。 有没有办法指定origin_country键仅在media_type等于tv时存在,或者键nametitle都是z.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())
    })

任何帮助表示赞赏!

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

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)
])

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

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

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

const schema = {
  page: 1,
  results: [],
  total_pages: 100,
  total_results: 200,
}

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

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(),
});

您可以在StackBlitz中看到完整的实现,并使用一些数据进行测试。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM