[英]Conditional keys based on value of another key with Zod
我正在用 TMDB API 做一个项目,并试图让它超级类型安全,以加强我正在学习的一些 TypeScript 的东西。 我正在使用 Zod 来描述 API 返回的数据的形状。
但是,我注意到根据请求参数,API 可以使用不同的密钥发回数据。 具体来说,如果 API 从data.media_type = "movie"
的“趋势”端点发回数据,它还有键title
、 original_title
和release_date
。 但是如果data.media_type = "tv"
,这三个键分别被重命名为name
、 original_name
和first_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
时存在,或者键name
或title
都是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.