Incorporating the TMDB API into my project, I am making an effort to enhance type safety by reinforcing some of the TypeScript concepts I am learning. To achieve this, I am utilizing Zod to define the structure of the data returned by the API.
Upon investigating, I have observed that depending on the request parameters, the API can return data with varying keys. Specifically, when the API responds with data from the "trending" endpoint where data.media_type = "movie"
, it includes keys such as title
, original_title
, and release_date
. However, if data.media_type = "tv"
, these three keys are replaced with name
, original_name
, and first_air_date
, along with the additional key origin_country
.
Consequently, I have defined the shape of my data as follows:
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()
})
To tackle this issue, I have applied the .optional()
method to each problematic key. Nevertheless, this approach lacks robustness in terms of type safety. Is there a method to specify that the existence of the origin_country
key is contingent upon the value of media_type
being equal to tv
, or to define the keys name
and title
as z.string()
based on certain conditions?
I should mention that the media_type
is also specified outside the retrieved data, specifically in the input to the API call (which appears as shown below, using 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())
})
Your assistance would be greatly appreciated!
Edit: Subsequent to posting this, I have come across the Zod method discriminatedUnion()
. However, I am encountering difficulties in implementing this method correctly. Currently, my implementation resembles the following:
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)
])
When invoking the request with any value for media_type
using the aforementioned code, an error message is displayed stating "Invalid discriminator value. Expected 'tv' | 'movie' | 'all' | 'person'".