Adding the new type to your front end code
Open the file with the EVENTS_QUERY
variable and find where you are doing the client.fetch()
call. Replace the more generic SanityDocument[]
type with your new type. Your code editor might support automatically importing the TypeScript type if you start typing out the name in between the <>
:
import { EVENTS_QUERYResult } from "@/sanity/types"
//... code
const events = await client.fetch<EVENTS_QUERYResult>(EVENTS_QUERY)
With this change, you might already get some new TypeScript errors in this file. For example, if you have followed the Day One course, you’ll find an error telling you that the event slug.current
might be null
.
Strictly, slug.current
can’t ever be null
in practice because of the defined(slug.current)
filter. Unfortunately, Sanity TypeGen doesn’t support null comparisons in filters, so you’ll have to check against slug
being null
.
Tweak your code like this to remove the TypeScript errors:
// src/app/page.tsximport Link from "next/link"import { groq } from "next-sanity"
import { client } from "@/sanity/client"import { EVENTS_QUERYResult } from "@/sanity/types"
const EVENTS_QUERY = groq`*[_type == "event" && slug.current != null]{_id, name, slug, date}|order(date desc)`
// Display Sanity content on the pageexport default async function IndexPage() { const events = await client.fetch<EVENTS_QUERYResult>(EVENTS_QUERY)
return ( <main className="flex min-h-screen flex-col items-center justify-between p-24"> <h2 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl text-center mb-8"> Events </h2> <ul className="grid grid-cols-1 gap-6 lg:grid-cols-2 xl:grid-cols-3 lg:gap-8"> {events.map((event) => ( <li className="event-card bg-white dark:bg-gray-950 p-4 rounded-lg shadow-md" key={event._id} > {event.slug && ( <Link className="hover:underline" href={`/events/${event.slug.current}`} > <h2 className="text-xl font-semibold">{event?.name}</h2> {event?.date && ( <p className="text-gray-500 dark:text-gray-400"> {new Date(event?.date).toLocaleDateString()} </p> )} </Link> )} </li> ))} </ul> </main> )}
Now, you can repeat the steps from above in the individual event route. Search and find the EVENT_QUERY
in your front end project and add the groq
template literal to it:
const EVENT_QUERY = groq`*[ _type == "event" && slug.current == $slug][0]{ ..., slug, headline->, venue->}`
Run sanity typegen generate
in your studio folder again. The output should say that it has made types for 2 GROQ queries.
Now, replace the SanityDocumentLike
type with the new EVENT_QUERYResult
type:
const event = await client.fetch<EVENT_QUERYResult>(EVENT_QUERY, params)
As with the other file, you might have some TypeScript error in your code editor. Primarily related to values potentially being null. You solve this by adjusting your code with null
(or undefined
) tests. This “defensive coding” is generally a better pattern, especially if you plan to implement content previews later where values are often lacking. Here is what the adjusted code looks like with the Next.js front end, which will be pretty similar to other frameworks:
import { PortableText } from "@portabletext/react"import { groq } from "next-sanity"import imageUrlBuilder from "@sanity/image-url"
import { SanityImageSource } from "@sanity/image-url/lib/types/types"import { client } from "@/sanity/client"import Link from "next/link"import { EVENT_QUERYResult } from "@/sanity/types"
const EVENT_QUERY = groq`*[ _type == "event" && slug.current == $slug ][0]{ ..., slug, headline->, venue->}`
const { projectId, dataset } = client.config()export const urlFor = (source: SanityImageSource) => projectId && dataset ? imageUrlBuilder({ projectId, dataset }).image(source) : null
export default async function EventPage({ params,}: { params: { slug: string }}) { const event = await client.fetch<EVENT_QUERYResult>(EVENT_QUERY, params) const { name, date, headline, image, details, format, doorsOpen, venue, tickets, } = event || {} const eventImageUrl = image?.asset ? urlFor(image)?.url() : null const artistImageUrl = headline?.photo?.asset ? urlFor(headline.photo)?.url() : null const eventDate = date && new Date(date).toDateString() const eventTime = date && new Date(date).toLocaleTimeString() const doorsOpenTime = date && doorsOpen && new Date(new Date(date).getTime() + doorsOpen * 60000).toLocaleTimeString()
return ( <main className="w-full min-h-screen py-12 md:py-24 lg:py-32"> <div className="container px-4 md:px-6 mx-auto"> <div className="mb-4"> <Link href="/">← Back to events</Link> </div> <div className="grid items-top gap-6 lg:grid-cols-[1fr_500px] lg:gap-12 xl:grid-cols-[1fr_550px]"> {(eventImageUrl || artistImageUrl) && ( // eslint-disable-next-line @next/next/no-img-element <img alt="Image" className="mx-auto aspect-video overflow-hidden rounded-xl object-cover object-center sm:w-full" height="310" src={eventImageUrl || artistImageUrl || ""} width="550" /> )} <div className="flex flex-col justify-center space-y-4"> <div className="space-y-2"> {format && ( <div className="inline-block rounded-lg bg-gray-100 px-3 py-1 text-sm dark:bg-gray-800 capitalize"> {format.replace("-", " ")} </div> )} {name && ( <h1 className="text-3xl font-bold tracking-tighter sm:text-5xl"> {name} </h1> )} {headline?.name && ( <dl className="grid grid-cols-2 gap-1 text-sm font-medium sm:gap-2 lg:text-base"> <div className="flex items-start"> <dt className="sr-only">Artist</dt> <dd className="font-semibold">Artist</dd> </div> <div className="grid gap-1"> <dt>{headline?.name}</dt> </div> </dl> )} <dl className="grid grid-cols-2 gap-1 text-sm font-medium sm:gap-2 lg:text-base"> <div className="flex items-start"> <dt className="sr-only">Date</dt> <dd className="font-semibold">Date</dd> </div> <div className="grid gap-1"> {eventDate && <dt>{eventDate}</dt>} {eventTime && <dt>{eventTime}</dt>} </div> </dl> {doorsOpenTime && ( <dl className="grid grid-cols-2 gap-1 text-sm font-medium sm:gap-2 lg:text-base"> <div className="flex items-start"> <dt className="sr-only">Doors Open</dt> <dd className="font-semibold">Doors Open</dd> </div> <div className="grid gap-1"> <dt>Doors Open</dt> <dt>{doorsOpenTime}</dt> </div> </dl> )} <dl className="grid grid-cols-2 gap-1 text-sm font-medium sm:gap-2 lg:text-base"> <div className="flex items-start"> <dt className="sr-only">Venue</dt> <dd className="font-semibold">Venue</dd> </div> <div className="grid gap-1"> <dt>{venue?.name}</dt> <dt> {venue?.city}, {venue?.country} </dt> </div> </dl> </div> {details && details.length > 0 && ( <div className="prose max-w-none"> <PortableText value={details} /> </div> )} {tickets && ( <div className="flex gap-4"> <a className="inline-flex h-10 items-center justify-center rounded-md bg-gray-900 w-1/2 text-sm font-medium text-gray-50 shadow transition-colors hover:bg-gray-900/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-950 disabled:pointer-events-none disabled:opacity-50 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-50/90 dark:focus-visible:ring-gray-300" href={tickets} > Buy Tickets </a> </div> )} </div> </div> </div> </main> )}